diff --git a/Resources/Skins/WebView2/@Resources/Calendar/index.html b/Resources/Skins/WebView2/@Resources/Calendar/index.html index bd3ba6b..604c454 100644 --- a/Resources/Skins/WebView2/@Resources/Calendar/index.html +++ b/Resources/Skins/WebView2/@Resources/Calendar/index.html @@ -31,12 +31,14 @@
- SuMoTuWeThFrSa
+
+

Hold Ctrl to Drag and Scroll to Resize.

+
diff --git a/Resources/Skins/WebView2/@Resources/Calendar/script.js b/Resources/Skins/WebView2/@Resources/Calendar/script.js index 69e0d64..688b196 100644 --- a/Resources/Skins/WebView2/@Resources/Calendar/script.js +++ b/Resources/Skins/WebView2/@Resources/Calendar/script.js @@ -1,66 +1,115 @@ +// ---- Helpers ---- +const loadVariable = (key, defaultValue) => { + const value = RainmeterAPI.GetVariable(String(key)); + if (value.includes("#")) { + writeVariable(String(key), String(defaultValue)); + return String(defaultValue); + } + return value; +}; + +const writeVariable = (key, value) => { + RainmeterAPI.Bang(`[!WriteKeyValue Variables "${key}" "${value}"]`); +}; + const date = new Date(); +const userLocale = Intl.DateTimeFormat().resolvedOptions().locale; + +let locale = loadVariable('locale', userLocale); + +function toggleLocale() { + locale = locale === userLocale ? 'en-US' : userLocale; + writeVariable('locale', locale); + renderWeekdays(); + renderCalendar(); +} + +const renderWeekdays = () => { + const weekdaysContainer = document.querySelector(".weekdays"); + weekdaysContainer.innerHTML = ""; + + const baseDate = new Date(Date.UTC(2026, 0, 5)); // Sunday + + const formatter = new Intl.DateTimeFormat(locale, { + weekday: 'short' + }); + + for (let i = 0; i < 7; i++) { + const day = new Date(baseDate); + day.setUTCDate(baseDate.getUTCDate() + i); + + const name = formatter.format(day); + weekdaysContainer.innerHTML += `${name}`; + } +}; const renderCalendar = () => { - date.setDate(1); - - const monthDays = document.querySelector(".days"); - - const lastDay = new Date( - date.getFullYear(), - date.getMonth() + 1, - 0 - ).getDate(); - - const prevLastDay = new Date( - date.getFullYear(), - date.getMonth(), - 0 - ).getDate(); - - const firstDayIndex = date.getDay(); - - const lastDayIndex = new Date( - date.getFullYear(), - date.getMonth() + 1, - 0 - ).getDay(); - - const nextDays = 7 - lastDayIndex - 1; - - const months = [ - "January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December" - ]; - - document.querySelector("#current-month").innerHTML = months[date.getMonth()]; - document.querySelector("#current-year").innerHTML = date.getFullYear(); - - let days = ""; - - // Previous month's days - for (let x = firstDayIndex; x > 0; x--) { - days += `
${prevLastDay - x + 1}
`; - } - - // Current month's days - for (let i = 1; i <= lastDay; i++) { - if ( - i === new Date().getDate() && - date.getMonth() === new Date().getMonth() && - date.getFullYear() === new Date().getFullYear() - ) { - days += `
${i}
`; - } else { - days += `
${i}
`; - } - } - - // Next month's days - for (let j = 1; j <= nextDays; j++) { - days += `
${j}
`; - } - - monthDays.innerHTML = days; + date.setDate(1); + + const monthDays = document.querySelector(".days"); + + const lastDay = new Date( + date.getFullYear(), + date.getMonth() + 1, + 0 + ).getDate(); + + const prevLastDay = new Date( + date.getFullYear(), + date.getMonth(), + 0 + ).getDate(); + + const firstDayIndex = + (new Intl.DateTimeFormat(locale, { weekday: 'short' }) + .formatToParts(date) + .find(p => p.type === 'weekday') ? date.getDay() : date.getDay()); + + const lastDayIndex = new Date( + date.getFullYear(), + date.getMonth() + 1, + 0 + ).getDay(); + + const nextDays = 7 - lastDayIndex - 1; + + // Localized month name + const monthFormatter = new Intl.DateTimeFormat(locale, { month: 'long' }); + + document.querySelector("#current-month").textContent = + monthFormatter.format(date); + + document.querySelector("#current-year").textContent = + date.getFullYear(); + + let days = ""; + + // Previous month's days + for (let x = firstDayIndex; x > 0; x--) { + days += `
${prevLastDay - x + 1}
`; + } + + // Current month's days + const today = new Date(); + + for (let i = 1; i <= lastDay; i++) { + if ( + i === today.getDate() && + date.getMonth() === today.getMonth() && + date.getFullYear() === today.getFullYear() + ) { + days += `
${i}
`; + } else { + days += `
${i}
`; + } + } + + // Next month's days + for (let j = 1; j <= nextDays; j++) { + days += `
${j}
`; + } + + monthDays.innerHTML = days; }; document.querySelector("#prev-month").addEventListener("click", () => { @@ -73,4 +122,5 @@ document.querySelector("#next-month").addEventListener("click", () => { renderCalendar(); }); +renderWeekdays(); renderCalendar(); diff --git a/Resources/Skins/WebView2/@Resources/Calendar/style.css b/Resources/Skins/WebView2/@Resources/Calendar/style.css index aa3420f..0566d21 100644 --- a/Resources/Skins/WebView2/@Resources/Calendar/style.css +++ b/Resources/Skins/WebView2/@Resources/Calendar/style.css @@ -1,10 +1,10 @@ :root { --bg-color: #0f0f13; --text-primary: #ffffff; - --text-secondary: #a0a0a0; + --text-secondary: #ebebeb; --accent-color: #4facfe; --accent-gradient: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); - --glass-bg: rgba(255, 255, 255, 0.05); + --glass-bg: rgba(0, 0, 0, 0.25); --glass-border: rgba(255, 255, 255, 0.1); --day-hover: rgba(255, 255, 255, 0.1); } @@ -18,7 +18,7 @@ body { background-color: transparent; font-family: 'Outfit', sans-serif; - height: 100vh; + height: 100%; display: flex; justify-content: center; align-items: center; @@ -26,6 +26,7 @@ body { user-select: none; /* Prevent text selection */ } + .widget-container { position: relative; width: 280px; @@ -93,7 +94,7 @@ body { border: 1px solid var(--glass-border); border-radius: 14px; /* Further reduced */ padding: 10px; /* Further reduced */ - box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); + box-shadow: 0 8px 12px 0 rgba(0, 0, 0, 0.37); display: flex; flex-direction: column; } @@ -107,6 +108,16 @@ body { color: var(--text-primary); } +.calendar-bottom { + position: absolute; + font-size: 0.8rem; /* Further reduced */ + display: flex; + align-items: center; + color: var(--text-secondary); + bottom: -20px; + left: 15px; +} + .month-year { font-size: 0.8rem; /* Further reduced */ font-weight: 600; diff --git a/Resources/Skins/WebView2/@Resources/Clock/script.js b/Resources/Skins/WebView2/@Resources/Clock/script.js index 6b20682..140ad20 100644 --- a/Resources/Skins/WebView2/@Resources/Clock/script.js +++ b/Resources/Skins/WebView2/@Resources/Clock/script.js @@ -1,32 +1,81 @@ +// ---- Helpers ---- +const loadVariable = (key, defaultValue) => { + const value = RainmeterAPI.GetVariable(String(key)); + if (value.includes("#")) { + writeVariable(String(key), String(defaultValue)); + return String(defaultValue); + } + return value; +}; + +const writeVariable = (key, value) => { + RainmeterAPI.Bang(`[!WriteKeyValue Variables "${key}" "${value}"]`); +}; + +// ---- Variables ---- +// We use Intl.DateTimeFormat().resolvedOptions to get the user's locale and timezone. +const userLocale = Intl.DateTimeFormat().resolvedOptions().locale; +const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + +let timeZone = loadVariable('timezone', userTimeZone); +let locale = loadVariable('locale', userLocale); +let format = loadVariable('format', '12h'); + +// ---- DOM cache ---- +const dom = { + hours: document.getElementById('hours'), + minutes: document.getElementById('minutes'), + seconds: document.getElementById('seconds'), + ampm: document.getElementById('ampm'), + date: document.getElementById('date') +}; + +// ---- Clock logic ---- +function toggleLocale() { + locale = locale === userLocale ? 'en-US' : userLocale; + writeVariable('locale', locale); + updateClock(); +} + +function toggleFormat() { + format = format === '12h' ? '24h' : '12h'; + writeVariable('format', format); + updateClock(); +} + function updateClock() { - const now = new Date(); - - let hours = now.getHours(); - const minutes = now.getMinutes(); - const seconds = now.getSeconds(); - const ampm = hours >= 12 ? 'PM' : 'AM'; - - // Convert to 12-hour format - hours = hours % 12; - hours = hours ? hours : 12; // the hour '0' should be '12' - - // Pad with leading zeros - const hoursStr = hours.toString().padStart(2, '0'); - const minutesStr = minutes.toString().padStart(2, '0'); - const secondsStr = seconds.toString().padStart(2, '0'); - - // Update DOM - document.getElementById('hours').textContent = hoursStr; - document.getElementById('minutes').textContent = minutesStr; - document.getElementById('seconds').textContent = secondsStr; - document.getElementById('ampm').textContent = ampm; - - // Update Date - const options = { weekday: 'long', month: 'long', day: 'numeric' }; - const dateStr = now.toLocaleDateString('en-US', options); - document.getElementById('date').textContent = dateStr; + const now = new Date(); + + const timeFormatter = new Intl.DateTimeFormat(locale, { + timeZone, + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: format === '12h' + }); + + const dateFormatter = new Intl.DateTimeFormat(locale, { + timeZone, + weekday: 'long', + month: 'long', + day: 'numeric' + }); + + const timeParts = timeFormatter.formatToParts(now); + const dateParts = dateFormatter.formatToParts(now); + + const get = (parts, type) => + parts.find(p => p.type === type)?.value || ''; + + dom.hours.textContent = get(timeParts, 'hour'); + dom.minutes.textContent = get(timeParts, 'minute'); + dom.seconds.textContent = get(timeParts, 'second'); + dom.ampm.textContent = get(timeParts, 'dayPeriod'); + + dom.date.textContent = dateParts.map(p => p.value).join(''); } -// Update immediately and then every second +// ---- Start clock ---- updateClock(); -setInterval(updateClock, 1000); +// Update clock once a second. +setInterval(updateClock, 1000); diff --git a/Resources/Skins/WebView2/@Resources/Clock/style.css b/Resources/Skins/WebView2/@Resources/Clock/style.css index 6fe6dea..13be3f3 100644 --- a/Resources/Skins/WebView2/@Resources/Clock/style.css +++ b/Resources/Skins/WebView2/@Resources/Clock/style.css @@ -1,11 +1,11 @@ :root { --bg-color: #0f0f13; --text-primary: #ffffff; - --text-secondary: #a0a0a0; + --text-secondary: #ebebeb; --blob-color-1: #4facfe; --blob-color-2: #00f2fe; --blob-color-3: #a18cd1; - --glass-bg: rgba(255, 255, 255, 0.05); + --glass-bg: rgba(0, 0, 0, 0.25); --glass-border: rgba(255, 255, 255, 0.1); } @@ -16,7 +16,6 @@ } body { - background-color: transparent; /* Transparent for widget usage */ font-family: 'Outfit', sans-serif; height: 100vh; display: flex; @@ -108,7 +107,7 @@ body { display: flex; justify-content: center; align-items: center; - box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); + box-shadow: 0 8px 12px 0 rgba(0, 0, 0, 0.37); } .clock-content { @@ -160,7 +159,7 @@ body { .date { font-size: 0.85rem; - font-weight: 300; + font-weight: 400; color: var(--text-secondary); letter-spacing: 1px; text-transform: uppercase; diff --git a/Resources/Skins/WebView2/@Resources/IslamicDate/style.css b/Resources/Skins/WebView2/@Resources/IslamicDate/style.css index fe14778..785f718 100644 --- a/Resources/Skins/WebView2/@Resources/IslamicDate/style.css +++ b/Resources/Skins/WebView2/@Resources/IslamicDate/style.css @@ -1,11 +1,11 @@ :root { --bg-color: #0f0f13; --text-primary: #ffffff; - --text-secondary: #a0a0a0; + --text-secondary: #ebebeb; --accent-purple: #764ba2; --accent-blue: #4facfe; --accent-cyan: #00f2fe; - --glass-bg: rgba(255, 255, 255, 0.05); + --glass-bg: rgba(0, 0, 0, 0.25); --glass-border: rgba(255, 255, 255, 0.1); } @@ -110,7 +110,7 @@ body { display: flex; justify-content: center; align-items: center; - box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); + box-shadow: 0 8px 12px 0 rgba(0, 0, 0, 0.37); } /* Content Wrapper - Centered Layout */ diff --git a/Resources/Skins/WebView2/@Resources/JSInteraction/script.js b/Resources/Skins/WebView2/@Resources/JSInteraction/script.js index e517c45..a089c3d 100644 --- a/Resources/Skins/WebView2/@Resources/JSInteraction/script.js +++ b/Resources/Skins/WebView2/@Resources/JSInteraction/script.js @@ -3,7 +3,7 @@ let updateCount = 0; // Called once when the plugin is ready window.OnInitialize = function() { log("🚀 OnInitialize called!"); - return "Initialized!"; + RainmeterAPI.Bang(`[!SetOption MeterStatus Text "Status: 🚀 OnInitialize called!"][!UpdateMeter MeterStatus][!Redraw]`) }; // Called on every Rainmeter update cycle @@ -17,8 +17,8 @@ window.OnUpdate = function() { display.textContent = message; } - // Return value updates the measure's string value in Rainmeter - return message; + RainmeterAPI.Bang(`[!SetOption MeterStatus Text "Status: ${message}"][!UpdateMeter MeterStatus][!Redraw]`) + }; // Function callable from Rainmeter via [Measure:CallJS('addNumbers', 'a', 'b')] diff --git a/Resources/Skins/WebView2/@Resources/Permissions/index.html b/Resources/Skins/WebView2/@Resources/Permissions/index.html new file mode 100644 index 0000000..2c17a2f --- /dev/null +++ b/Resources/Skins/WebView2/@Resources/Permissions/index.html @@ -0,0 +1,166 @@ + + + + + + Web Permissions Demo + + + + +

WebView2 Permission Tests

+ +
+ + +
+
+ Camera + navigator.mediaDevices / Permissions API +
+
+ + +
+ + +
+
+ Microphone + navigator.mediaDevices / Permissions API +
+
+ + +
+ + +
+
+ Geolocation + navigator.geolocation +
+
+ + +

This permission requires Windows Location Services to be enabled in your machine to work.

+

Go to Windows Settings -> Privacy & security -> Location and enable "Location services", "Let apps access your location" and "Let desktop apps access your location".

+
+ + +
+
+ Notifications + Notification API +
+
+ + +

This permission will not trigger a prompt, it has to be granted manually on the WebView Measure by setting AllowNotifications=1

+
+ + +
+
+ Clipboard + navigator.clipboard +
+
+ + +
+ + +
+
+ Persistent Storage + navigator.storage +
+
+ + +
+ + +
+
+ Screen Wake Lock + Wake Lock API +
+
+ + +
+ + +
+
+ Bluetooth + Web Bluetooth API +
+
+ + +
+ + +
+
+ USB + WebUSB API +
+
+ + +
+ + +
+
+ Serial + Web Serial API +
+
+ + +
+ + +
+
+ HID + WebHID API +
+
+ + +
+ + +
+
+ Idle Detection + Idle Detection API +
+
+ + +
+ + +
+
+ Clipboard Write + navigator.clipboard.writeText +
+
+ + +
+ + +
+ + + + diff --git a/Resources/Skins/WebView2/@Resources/Permissions/script.js b/Resources/Skins/WebView2/@Resources/Permissions/script.js new file mode 100644 index 0000000..dc0102d --- /dev/null +++ b/Resources/Skins/WebView2/@Resources/Permissions/script.js @@ -0,0 +1,344 @@ +function setStatus(id, text, cls) { + const el = document.getElementById(id); + el.textContent = text; + el.className = `status ${cls || ''}`.trim(); +} + +async function queryPermission(name) { + if (!navigator.permissions) { + throw new Error('Permissions API not supported'); + } + const status = await navigator.permissions.query({ name }); + return status.state; +} + +/* ---------- Camera ---------- */ + +async function checkCamera() { + setStatus('camera-status', 'Checking...', 'loading'); + try { + const state = await queryPermission('camera'); + setStatus('camera-status', `State: ${state}`, 'success'); + } catch (e) { + setStatus('camera-status', e.message || 'Error', 'error'); + } +} + +async function requestCamera() { + setStatus('camera-status', 'Requesting...', 'loading'); + try { + await navigator.mediaDevices.getUserMedia({ video: true }); + setStatus('camera-status', 'Granted', 'success'); + } catch (e) { + setStatus('camera-status', 'Denied', 'error'); + } +} + +/* ---------- Microphone ---------- */ + +async function checkMicrophone() { + setStatus('mic-status', 'Checking...', 'loading'); + try { + const state = await queryPermission('microphone'); + setStatus('mic-status', `State: ${state}`, 'success'); + } catch (e) { + setStatus('mic-status', e.message || 'Error', 'error'); + } +} + +async function requestMicrophone() { + setStatus('mic-status', 'Requesting...', 'loading'); + try { + await navigator.mediaDevices.getUserMedia({ audio: true }); + setStatus('mic-status', 'Granted', 'success'); + } catch (e) { + setStatus('mic-status', 'Denied', 'error'); + } +} + +/* ---------- Geolocation ---------- */ + +async function checkGeolocation() { + setStatus('geo-status', 'Checking...', 'loading'); + try { + const state = await queryPermission('geolocation'); + setStatus('geo-status', `State: ${state}`, 'success'); + } catch (e) { + setStatus('geo-status', e.message || 'Error', 'error'); + } +} + +async function requestGeolocation() { + setStatus('geo-status', 'Requesting...', 'loading'); + try { + navigator.geolocation.getCurrentPosition( + () => setStatus('geo-status', 'Granted', 'success'), + () => setStatus('geo-status', 'Denied', 'error') + ); + } catch (e) { + setStatus('geo-status', e.message || 'Error', 'error'); + } +} + +/* ---------- Notifications ---------- */ + +async function checkNotifications() { + setStatus('notif-status', 'Checking...', 'loading'); + try { + const state = Notification.permission; + setStatus('notif-status', `State: ${state}`, 'success'); + } catch (e) { + setStatus('notif-status', e.message || 'Error', 'error'); + } +} + +async function requestNotifications() { + setStatus('notif-status', 'Requesting...', 'loading'); + try { + const result = await Notification.requestPermission(); + setStatus('notif-status', `${result == 'denied' ? 'Denied' : 'Granted'}`, `${result == 'denied' ? 'error' : 'success'}`); + } catch (e) { + setStatus('notif-status', e.message || 'Error', 'error'); + } +} + +/* ---------- Clipboard ---------- */ + +async function checkClipboard() { + setStatus('clipboard-status', 'Checking...', 'loading'); + try { + const state = await queryPermission('clipboard-read'); + setStatus('clipboard-status', `State: ${state}`, 'success'); + } catch (e) { + setStatus('clipboard-status', e.message || 'Error', 'error'); + } +} + +async function requestClipboard() { + setStatus('clipboard-status', 'Requesting...', 'loading'); + try { + await navigator.clipboard.readText(); + setStatus('clipboard-status', 'Granted', 'success'); + } catch (e) { + setStatus('clipboard-status', 'Denied', 'error'); + } +} + +/* ---------- Persistent Storage ---------- */ + +async function checkStorage() { + setStatus('storage-status', 'Checking...', 'loading'); + try { + const granted = await navigator.storage.persisted(); + setStatus('storage-status', granted ? 'Granted' : 'Not granted', granted ? 'success' : ''); + } catch (e) { + setStatus('storage-status', e.message, 'error'); + } +} + +async function requestStorage() { + setStatus('storage-status', 'Requesting...', 'loading'); + try { + const granted = await navigator.storage.persist(); + setStatus('storage-status', granted ? 'Granted' : 'Denied', granted ? 'success' : 'error'); + } catch (e) { + setStatus('storage-status', e.message, 'error'); + } +} + +/* ---------- Wake Lock ---------- */ + +let wakeLock; + +async function checkWakeLock() { + setStatus('wakelock-status', 'Supported', 'success'); +} + +async function requestWakeLock() { + setStatus('wakelock-status', 'Requesting...', 'loading'); + try { + wakeLock = await navigator.wakeLock.request('screen'); + setStatus('wakelock-status', 'Granted', 'success'); + } catch (e) { + setStatus('wakelock-status', 'Denied', 'error'); + } +} + +/* ---------- Bluetooth ---------- */ + +async function checkBluetooth() { + setStatus('bluetooth-status', 'Supported', 'success'); +} + +async function requestBluetooth() { + setStatus('bluetooth-status', 'Requesting...', 'loading'); + try { + await navigator.bluetooth.requestDevice({ acceptAllDevices: true }); + setStatus('bluetooth-status', 'Granted', 'success'); + } catch { + setStatus('bluetooth-status', 'Denied', 'error'); + } +} + +/* ---------- USB ---------- */ + +async function checkUSB() { + setStatus('usb-status', 'Supported', 'success'); +} + +async function requestUSB() { + setStatus('usb-status', 'Requesting...', 'loading'); + try { + await navigator.usb.requestDevice({ filters: [] }); + setStatus('usb-status', 'Granted', 'success'); + } catch { + setStatus('usb-status', 'Denied', 'error'); + } +} + +/* ---------- Serial ---------- */ + +async function checkSerial() { + setStatus('serial-status', 'Supported', 'success'); +} + +async function requestSerial() { + setStatus('serial-status', 'Requesting...', 'loading'); + try { + await navigator.serial.requestPort(); + setStatus('serial-status', 'Granted', 'success'); + } catch { + setStatus('serial-status', 'Denied', 'error'); + } +} + +/* ---------- HID ---------- */ + +async function checkHID() { + setStatus('hid-status', 'Supported', 'success'); +} + +async function requestHID() { + setStatus('hid-status', 'Requesting...', 'loading'); + try { + await navigator.hid.requestDevice({ filters: [] }); + setStatus('hid-status', 'Granted', 'success'); + } catch { + setStatus('hid-status', 'Denied', 'error'); + } +} + +/* ---------- Idle Detection ---------- */ + +async function checkIdle() { + setStatus('idle-status', 'Checking...', 'loading'); + try { + const state = await queryPermission('idle-detection'); + setStatus('idle-status', `State: ${state}`, 'success'); + } catch (e) { + setStatus('idle-status', e.message, 'error'); + } +} + +async function requestIdle() { + setStatus('idle-status', 'Requesting...', 'loading'); + + try { + const permission = await IdleDetector.requestPermission(); + + if (permission === 'granted') { + setStatus('idle-status', 'Granted', 'success'); + } else { + setStatus('idle-status', 'Denied', 'error'); + } + } catch (err) { + setStatus('idle-status', 'Error requesting permission', 'error'); + } +} + + +/* ---------- Clipboard Write ---------- */ + +async function checkClipboardWrite() { + setStatus('clipboard-write-status', 'Checking...', 'loading'); + try { + const state = await queryPermission('clipboard-write'); + setStatus('clipboard-write-status', `State: ${state}`, 'success'); + } catch (e) { + setStatus('clipboard-write-status', e.message, 'error'); + } +} + +async function requestClipboardWrite() { + setStatus('clipboard-write-status', 'Requesting...', 'loading'); + try { + await navigator.clipboard.writeText('Test'); + setStatus('clipboard-write-status', 'Granted', 'success'); + } catch { + setStatus('clipboard-write-status', 'Denied', 'error'); + } +} + + +/* ---------- Bindings ---------- */ + +function bind() { + document.getElementById('camera-check-btn').addEventListener('click', checkCamera); + document.getElementById('camera-request-btn').addEventListener('click', requestCamera); + + document.getElementById('mic-check-btn').addEventListener('click', checkMicrophone); + document.getElementById('mic-request-btn').addEventListener('click', requestMicrophone); + + document.getElementById('geo-check-btn').addEventListener('click', checkGeolocation); + document.getElementById('geo-request-btn').addEventListener('click', requestGeolocation); + + document.getElementById('notif-check-btn').addEventListener('click', checkNotifications); + document.getElementById('notif-request-btn').addEventListener('click', requestNotifications); + + document.getElementById('clipboard-check-btn').addEventListener('click', checkClipboard); + document.getElementById('clipboard-request-btn').addEventListener('click', requestClipboard); + + document.getElementById('storage-check-btn').onclick = checkStorage; + document.getElementById('storage-request-btn').onclick = requestStorage; + + document.getElementById('wakelock-check-btn').onclick = checkWakeLock; + document.getElementById('wakelock-request-btn').onclick = requestWakeLock; + + document.getElementById('bluetooth-check-btn').onclick = checkBluetooth; + document.getElementById('bluetooth-request-btn').onclick = requestBluetooth; + + document.getElementById('usb-check-btn').onclick = checkUSB; + document.getElementById('usb-request-btn').onclick = requestUSB; + + document.getElementById('serial-check-btn').onclick = checkSerial; + document.getElementById('serial-request-btn').onclick = requestSerial; + + document.getElementById('hid-check-btn').onclick = checkHID; + document.getElementById('hid-request-btn').onclick = requestHID; + + document.getElementById('idle-check-btn').onclick = checkIdle; + document.getElementById('idle-request-btn').onclick = requestIdle; + + document.getElementById('clipboard-write-check-btn').onclick = checkClipboardWrite; + document.getElementById('clipboard-write-request-btn').onclick = requestClipboardWrite; + +} + +window.addEventListener('DOMContentLoaded', () => { + bind(); + + const msg = 'Ready'; + setStatus('camera-status', msg); + setStatus('mic-status', msg); + setStatus('geo-status', msg); + setStatus('notif-status', msg); + setStatus('clipboard-status', msg); + setStatus('storage-status', msg); + setStatus('wakelock-status', msg); + setStatus('bluetooth-status', msg); + setStatus('usb-status', msg); + setStatus('serial-status', msg); + setStatus('hid-status', msg); + setStatus('idle-status', msg); + setStatus('clipboard-write-status', msg); +}); diff --git a/Resources/Skins/WebView2/@Resources/Permissions/style.css b/Resources/Skins/WebView2/@Resources/Permissions/style.css new file mode 100644 index 0000000..c8c9b36 --- /dev/null +++ b/Resources/Skins/WebView2/@Resources/Permissions/style.css @@ -0,0 +1,54 @@ +:root { + --primary: #6366f1; + --secondary: #a855f7; + --bg: #0f172a; + --card-bg: rgba(30, 41, 59, 0.7); + --text: #f8fafc; + --text-muted: #94a3b8; + --success: #22c55e; + --error: #ef4444; +} + +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: 'Segoe UI', system-ui, sans-serif; + background: var(--bg); + color: var(--text); + height: 100vh; + overflow: hidden; + display: flex; + flex-direction: column; + padding: 20px; +} + +h1 { + font-size: 1.5rem; + margin-bottom: 1rem; + background: linear-gradient(to right, var(--primary), var(--secondary)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + text-align: center; +} + +.container { flex: 1; display: flex; flex-direction: column; gap: 1rem; overflow-y: auto; padding-right: 5px; } +.card { background: var(--card-bg); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 1rem; transition: transform 0.2s; } +.card:hover { transform: translateY(-2px); border-color: rgba(255,255,255,0.2); } +.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; } +.card-title { font-weight: 600; color: var(--text); } +.card-subtitle { font-size: 0.8rem; color: var(--text-muted); } +.value-display { font-family: 'Consolas', monospace; background: rgba(0,0,0,0.3); padding: 0.5rem; border-radius: 6px; color: var(--success); word-break: break-all; } +.btn { background: linear-gradient(135deg, var(--primary), var(--secondary)); border: none; color: white; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-weight: 600; transition: opacity 0.2s; margin-top: 0.75rem; width: 100%; } +.btn:hover { opacity: 0.9; } +.status { font-size: 0.8rem; margin-top: 0.5rem; text-align: right; } +.status.loading { color: var(--text-muted); } +.status.success { color: var(--success); } +.status.error { color: var(--error); } +.input { width: 100%; padding: 0.5rem; margin: 0.25rem 0; border-radius: 6px; border: 1px solid rgba(255,255,255,0.15); background: rgba(0,0,0,0.2); color: var(--text); } +.select { width: 100%; padding: 0.5rem; margin: 0.25rem 0; border-radius: 6px; border: 1px solid rgba(255,255,255,0.15); background: rgba(0,0,0,0.2); color: var(--text); } + +::-webkit-scrollbar { width: 8px; height: 8px; } +::-webkit-scrollbar-track { background: rgba(255,255,255,0.05); border-radius: 4px; } +::-webkit-scrollbar-thumb { background: linear-gradient(to bottom, var(--primary), var(--secondary)); border-radius: 4px; border: 2px solid transparent; background-clip: content-box; } +::-webkit-scrollbar-thumb:hover { background: linear-gradient(to bottom, var(--secondary), var(--primary)); border: 2px solid transparent; background-clip: content-box; } +::-webkit-scrollbar-corner { background: transparent; } diff --git a/Resources/Skins/WebView2/@Resources/ReadSectionOption/script.js b/Resources/Skins/WebView2/@Resources/ReadSectionOption/script.js index f2407f3..599cc0d 100644 --- a/Resources/Skins/WebView2/@Resources/ReadSectionOption/script.js +++ b/Resources/Skins/WebView2/@Resources/ReadSectionOption/script.js @@ -12,7 +12,7 @@ async function refreshValues() { // 3. Read Measure Int (Int) updateField('measure-int', async () => { - const val = await RainmeterAPI.ReadIntFromSection('MeasureCalc', 'Formula', 0); + const val = await RainmeterAPI.ReplaceVariables('[MeasureCalc]'); return val; }); diff --git a/Resources/Skins/WebView2/@Resources/Weather/script.js b/Resources/Skins/WebView2/@Resources/Weather/script.js index 2421c1d..2ff8ec0 100644 --- a/Resources/Skins/WebView2/@Resources/Weather/script.js +++ b/Resources/Skins/WebView2/@Resources/Weather/script.js @@ -1,6 +1,6 @@ // Weather Widget Script const API_KEY = 'd87d34d2cddf097a3710c1627a0d024d'; // Users can get free API key from openweathermap.org -const USE_GEOLOCATION = false; // Set to true to use browser geolocation (may not work in WebView2) +const USE_GEOLOCATION = false; // Set to true to use browser geolocation (requires windows location to be enabled) // Manual coordinates - UPDATE THESE TO YOUR LOCATION const MANUAL_LAT = 29.712885630493965; diff --git a/Resources/Skins/WebView2/@Resources/Weather/style.css b/Resources/Skins/WebView2/@Resources/Weather/style.css index e0c2b93..6b2cfb7 100644 --- a/Resources/Skins/WebView2/@Resources/Weather/style.css +++ b/Resources/Skins/WebView2/@Resources/Weather/style.css @@ -1,11 +1,11 @@ :root { --bg-color: #0f0f13; --text-primary: #ffffff; - --text-secondary: #a0a0a0; + --text-secondary: #ebebeb; --accent-blue: #4facfe; --accent-cyan: #00f2fe; --accent-purple: #a18cd1; - --glass-bg: rgba(255, 255, 255, 0.05); + --glass-bg: rgba(0, 0, 0, 0.25); --glass-border: rgba(255, 255, 255, 0.1); } @@ -18,7 +18,7 @@ body { background-color: transparent; font-family: 'Outfit', sans-serif; - height: 100vh; + height: 100%; display: flex; justify-content: center; align-items: center; @@ -43,8 +43,8 @@ body { top: 50%; left: 50%; transform: translate(-50%, -50%); - width: 90%; /* Match card width */ - height: 85%; /* Match card height */ + width: 80%; /* Match card width */ + height: 75%; /* Match card height */ z-index: 0; overflow: hidden; border-radius: 16px; /* Match card border-radius */ @@ -99,15 +99,15 @@ body { .weather-card { position: relative; z-index: 1; - width: 90%; - height: 85%; + width: 80%; + height: 75%; background: var(--glass-bg); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border: 1px solid var(--glass-border); border-radius: 16px; padding: 12px; - box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); + box-shadow: 0 8px 12px 0 rgba(0, 0, 0, 0.37); display: flex; flex-direction: column; } diff --git a/Resources/Skins/WebView2/@Resources/YoutubePlayer/index.html b/Resources/Skins/WebView2/@Resources/YoutubePlayer/index.html new file mode 100644 index 0000000..20d8aea --- /dev/null +++ b/Resources/Skins/WebView2/@Resources/YoutubePlayer/index.html @@ -0,0 +1,105 @@ + + + + + + + YouTube Player + + + + + + +
+
+ + +
+
+

🗖

+
+
+

X

+
+
+
+
+
+
+ + +
+
+
+
+
+ + + + + + + + diff --git a/Resources/Skins/WebView2/@Resources/YoutubePlayer/script.js b/Resources/Skins/WebView2/@Resources/YoutubePlayer/script.js new file mode 100644 index 0000000..918be04 Binary files /dev/null and b/Resources/Skins/WebView2/@Resources/YoutubePlayer/script.js differ diff --git a/Resources/Skins/WebView2/@Resources/YoutubePlayer/style.css b/Resources/Skins/WebView2/@Resources/YoutubePlayer/style.css new file mode 100644 index 0000000..4518db9 --- /dev/null +++ b/Resources/Skins/WebView2/@Resources/YoutubePlayer/style.css @@ -0,0 +1,372 @@ +/* ============================================================================ + Root variables +============================================================================ */ +:root { + /* ===================== + Colors + ===================== */ + --bg-color: #0f0f0f; + --bg-hover: #272727; + + --accent-danger: #ff0000; + --yt-red: #fe0f38; + + --text-primary: #ffffff; + --text-secondary: #868685; + + --text-button-primary: #868685; + --text-button-secondary: #ffffff; + + --icon-primary: #ffffff; + --icon-on-accent: #ffffff; + + --border-muted: rgba(125, 125, 125, 0.2); + --border-focus: rgba(255, 255, 255, 0.1); + + --shadow-focus: rgba(255, 255, 255, 0.15); + --soft-shadow: rgba(0, 0, 0, 0.28); + --yt-glow: rgba(255, 255, 255, 0.25); + + /* ===================== + Typography + ===================== */ + --font-family: 'Outfit', sans-serif; + --font-size-xs: 13px; + --font-size-sm: 14px; + + /* ===================== + Spacing + ===================== */ + --space-xs: 8px; + --space-sm: 10px; + --space-md: 12px; + + /* ===================== + Layout sizes + ===================== */ + --header-height: 45px; + --footer-height: 50px; + --control-size: 30px; + --input-size: 30px; + --icon-size: 18px; + + /* ===================== + UI behavior + ===================== */ + --radius-sm: 15px; + --transition-fast: 0.15s ease; +} + +/* ============================================================================ + Light theme override +============================================================================ */ +:root[data-theme="light"] { + --bg-color: #ffffff; + --bg-hover: #f2f2f2; + + --text-primary: #111111; + --text-secondary: #444444; + + --text-button-primary: #111111; + + --accent-danger: #cc0000; + --yt-red: #e60023; + --yt-glow: rgba(125, 125, 125, 0.35); + + --icon-primary: #111111; + --icon-on-accent: #ffffff; + + --border-muted: rgba(0, 0, 0, 0.15); + --border-focus: rgba(0, 0, 0, 0.25); + --shadow-focus: rgba(0, 0, 0, 0.15); + --soft-shadow: rgba(0, 0, 0, 0.15); +} + +/* ============================================================================ + Reset & base styles +============================================================================ */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + transition: + background-color var(--transition-fast), + color var(--transition-fast), + border-color var(--transition-fast), + box-shadow var(--transition-fast); +} + +body { + height: 100vh; + display: flex; + overflow: hidden; + font-family: var(--font-family); + user-select: none; +} + +/* ============================================================================ + Layout containers +============================================================================ */ +.widget-container { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + width: 100%; + height: 100%; +} + +.head, +.foot { + width: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +/* --------------------------------------------------------------------------- + Header (draggable region) +--------------------------------------------------------------------------- */ +.head { + app-region: drag; /* Rainmeter / WebView2 drag region */ + height: var(--header-height); + padding: var(--space-sm); +} + +.title { + position: absolute; + left: 50%; + transform: translateX(-50%); + + display: flex; + justify-content: center; + user-select: none; +} + +/* --------------------------------------------------------------------------- + Footer +--------------------------------------------------------------------------- */ +.foot { + height: var(--footer-height); + padding: var(--space-md); +} + +/* ============================================================================ + Title & branding +============================================================================ */ +/* Player title text */ +h5 { + color: var(--text-secondary); + margin-bottom: var(--space-xs); + padding-left: var(--space-xs); + user-select: none; +} + +/* YouTube logo container */ +.yt-logo { + width: 93px; + height: 20px; + display: inline-flex; + align-items: center; +} + +/* SVG scaling */ +.yt-logo svg { + width: 100%; + height: 100%; +} + +/* SVG theming */ +.yt-logo .yt-bg { fill: var(--yt-red); } +.yt-logo .yt-play { fill: var(--icon-on-accent); } +.yt-logo .yt-text path { fill: var(--icon-primary); } + +/* Subtle glow on hover */ +.title:hover { + filter: drop-shadow(0 0 3px var(--yt-glow)); +} + +/* ============================================================================ + Window controls (maximize / exit / theme) +============================================================================ */ +.window-controls { + display: flex; + align-items: center; + margin-left: auto; + app-region: no-drag; +} + +/* Shared button styles */ +.exit, +.maximize, +.theme-toggle { + width: var(--control-size); + height: var(--control-size); + + font-size: var(--font-size-xs); + + display: flex; + align-items: center; + justify-content: center; + + background: none; + border: none; + border-radius: var(--radius-sm); + + color: var(--text-secondary); + cursor: pointer; + user-select: none; + + app-region: no-drag; + transition: background-color var(--transition-fast), + color var(--transition-fast); +} + +/* Hover states */ +.exit:hover { + background-color: var(--bg-hover); + color: var(--accent-danger); +} + +.maximize:hover { + background-color: var(--bg-hover); + color: var(--text-primary); +} + +[data-theme="light"] .moon { + display: none; +} + +[data-theme="dark"] .sun { + display: none; +} + +.icon { + display: inline-flex; + width: var(--icon-size); + height: var(--icon-size); +} + +/* ============================================================================ + Player container +============================================================================ */ +.player { + flex: 1; + width: 100%; + height: 100%; + + padding: 1px; + border: 1px solid var(--border-muted); + border-radius: var(--radius-sm); +} + +/* Disable iframe interaction while resizing */ +.player.no-pointer { + pointer-events: none; +} + +/* ============================================================================ + Form elements +============================================================================ */ +input, .load-button { + + display: flex; + align-items: center; + justify-content: center; + + height: var(--input-size); + padding: var(--space-sm); + + font-size: var(--font-size-sm); + font-weight: 600; + + border-radius: var(--radius-sm); + + color: var(--text-secondary); +} + +input { + width: 90%; + + background-color: var(--bg-color); + outline: none; + + box-shadow: inset 0 2px 8px var(--soft-shadow); + app-region: no-drag; + + border: 1px solid var(--border-muted); + + transition: border-color var(--transition-fast), + box-shadow var(--transition-fast); +} + +input:focus { + border-color: var(--border-focus); + box-shadow: 0 0 4px var(--shadow-focus); +} + +.load-button { + flex: 1; + margin-left: var(--space-md); + + background-color: var(--bg-hover); + border: none; + + cursor: pointer; + user-select: none; + + transition: background-color var(--transition-fast), + transform var(--transition-fast); +} + +.load-button:hover, +.theme-toggle:hover { + background-color: var(--yt-red); + color: var(--text-button-secondary); +} + +.load-button:active, +.theme-toggle:active { + transform: scale(0.98); +} + +/* ============================================================================ + Resize handles (used by JS resizing logic) +============================================================================ */ +#box { + position: relative; + border: 2px solid transparent; +} + +/* Invisible resize hit areas */ +.handle { + position: absolute; + background: transparent; + app-region: no-drag; +} + +.right { + right: -5px; + top: 0; + width: 10px; + height: 100%; + cursor: ew-resize; +} + +.bottom { + bottom: -5px; + left: 0; + width: 100%; + height: 10px; + cursor: ns-resize; +} + +.corner { + right: -5px; + bottom: -5px; + width: 15px; + height: 15px; + cursor: nwse-resize; +} diff --git a/Resources/Skins/WebView2/BangCommand/BangCommand.ini b/Resources/Skins/WebView2/BangCommand/BangCommand.ini index 54485b4..033bd01 100644 Binary files a/Resources/Skins/WebView2/BangCommand/BangCommand.ini and b/Resources/Skins/WebView2/BangCommand/BangCommand.ini differ diff --git a/Resources/Skins/WebView2/Calendar/Calendar.ini b/Resources/Skins/WebView2/Calendar/Calendar.ini index 65c57a6..489531b 100644 --- a/Resources/Skins/WebView2/Calendar/Calendar.ini +++ b/Resources/Skins/WebView2/Calendar/Calendar.ini @@ -1,26 +1,57 @@ [Rainmeter] -Update=1000 +;Since this is a pure WebView2 skin, we don't need to update the skin. +Update=-1 +; Add useful commands to the Skin Menu. +; We use ExecuteScript command to toggle locale. +; Check implementation at @Resources\Calendar\script.js +ContextTitle=Toggle Locale +ContextAction=[!CommandMeasure WebView2 "ExecuteScript toggleLocale();"] +ContextTitle2=Open DevTools +ContextAction2=[!CommandMeasure WebView2 "Open DevTools"] +ContextTitle3= Open Task Manager +ContextAction3=[!CommandMeasure WebView2 "Open TaskManager"] +;Save the scale value when the skin is unloaded or when Rainmeter exits. +OnCloseAction=[!WriteKeyValue Variables Scale [#Scale]] [Metadata] Name=Calendar Author=nstechbytes Information=Calendar Widget using WebView2 -Version=0.0.7 +Version=0.0.8 License=Creative Commons Attribution-Non-Commercial-Share Alike 3.0 + +[Variables] +Scale=1 + ; ======================================== ; Measure ; ======================================== +;Create your WebView2 measure [WebView2] Measure=Plugin Plugin=WebView2 URL=#@#Calendar\index.html -W=280 -H=350 -X=15 -Y=15 +W=(280*#Scale#) +H=(300*#Scale#) +X=0 +Y=0 +;Add options to make the skin non-interactable and scalable while holding CTRL. +Clickthrough=2 +ZoomFactor=#Scale# +;Disable internal ZoomControl +ZoomControl=0 +;Disable Assistive Features (Print, Caret Browsing and Find) +AssistiveFeatures=0 +DynamicVariables=1 + ; ======================================== ; Background ; ======================================== +;Create a background that covers the entire WebView2 area. [Background_Shape] Meter=Shape -Shape=Rectangle 0,0,310,400 | StrokeWidth 0 | FillColor 0,0,0,1 +Shape=Rectangle 0,0,(280*#Scale#),(300*#Scale#) | StrokeWidth 0 | FillColor 0,0,0,1 +;Set Mouse actions to change scale by CTRL + scrolling. +MouseScrollUpAction=[!SetVariable Scale "(Clamp(#Scale#+0.1,0.5,2.5))"][!UpdateMeter #CURRENTSECTION#][!UpdateMeasure WebView2][!Redraw] +MouseScrollDownAction=[!SetVariable Scale "(Clamp(#Scale#-0.1,0.5,2.5))"][!UpdateMeter #CURRENTSECTION#][!UpdateMeasure WebView2][!Redraw] +DynamicVariables=1 diff --git a/Resources/Skins/WebView2/Clock/Clock.ini b/Resources/Skins/WebView2/Clock/Clock.ini index 2b61529..2cb36e4 100644 --- a/Resources/Skins/WebView2/Clock/Clock.ini +++ b/Resources/Skins/WebView2/Clock/Clock.ini @@ -1,27 +1,68 @@ [Rainmeter] -Update=1000 +;Since this is a pure WebView2 skin, we don't need to update the skin. +Update=-1 +; Add useful commands to the Skin Menu. +; We use ExecuteScript command to toggle format and locale. +; Check implementation at @Resources\Clock\script.js +ContextTitle= Toggle Format +ContextAction=[!CommandMeasure WebView2 "ExecuteScript toggleFormat();"] +ContextTitle2=Toggle Locale +ContextAction2=[!CommandMeasure WebView2 "ExecuteScript toggleLocale();"] +; Other useful commands: +ContextTitle3=Open DevTools +ContextAction3=[!CommandMeasure WebView2 "Open DevTools"] +ContextTitle4= Open Task Manager +ContextAction4=[!CommandMeasure WebView2 "Open TaskManager"] +ContextTitle5= Reload +ContextAction5=[!CommandMeasure WebView2 "Navigate Reload"] +ContextTitle6= --- +ContextAction6=[] +ContextTitle7= Refresh Skin +ContextAction7=[!Refresh] +ContextTitle8= Skin Menu +ContextAction8=[!SkinMenu] +;Save the scale value when the skin is unloaded or when Rainmeter exits. +OnCloseAction=[!WriteKeyValue Variables Scale [#Scale]] [Metadata] Name=Calendar Author=nstechbytes -Information=Calendar Widget using WebView2 -Version=0.0.7 +Information=Clock Widget using WebView2 +Version=0.0.8 License=Creative Commons Attribution-Non-Commercial-Share Alike 3.0 + +[Variables] +Scale=1 + ; ======================================== ; Measure ; ======================================== +;Create your WebView2 measure [WebView2] Measure=Plugin Plugin=WebView2 +;Set your initial URL and dimensions. URL=#@#Clock\index.html -W=280 -H=160 -X=15 -Y=15 +W=(280*#Scale#) +H=(160*#Scale#) +X=0 +Y=0 +;Add options to make the skin non-interactable and scalable. +Clickthrough=1 +ZoomFactor=[#Scale] +DynamicVariables=1 + ; ======================================== ; Background ; ======================================== +;Create a background that covers the entire WebView2 area. [Background_Shape] Meter=Shape -Shape=Rectangle 0,0,310,190 | StrokeWidth 0 | FillColor 0,0,0,1 - +Shape=Rectangle 0,0,(280*#Scale#),(160*#Scale#) | StrokeWidth 0 | FillColor 0,0,0,1 +;Set Mouse actions to change scale by scrolling. +MouseScrollUpAction=[!SetVariable Scale "(Clamp(#Scale#+0.1,0.5,2.5))"][!UpdateMeasure WebView2][!UpdateMeter Background_Shape][!Redraw] +MouseScrollDownAction=[!SetVariable Scale "(Clamp(#Scale#-0.1,0.5,2.5))"][!UpdateMeasure WebView2][!UpdateMeter Background_Shape][!Redraw] +;Set Mouse Action to Open the Skin Custom Menu. +RightMouseUpAction=[!SkinCustomMenu] +MouseActionCursor="None" +DynamicVariables=1 diff --git a/Resources/Skins/WebView2/InformationProperty/InformationProperty.ini b/Resources/Skins/WebView2/InformationProperty/InformationProperty.ini index dc7f22e..d5d3db8 100644 --- a/Resources/Skins/WebView2/InformationProperty/InformationProperty.ini +++ b/Resources/Skins/WebView2/InformationProperty/InformationProperty.ini @@ -7,7 +7,7 @@ DynamicWindowSize=1 Name=InformationProperty Author=nstechbytes Information=Shows Rainmeter information properties via WebView2. -Version=0.0.7 +Version=0.0.8 License=Creative Commons Attribution-Non-Commercial-Share Alike 3.0 ; ======================================== ; Measure @@ -18,11 +18,12 @@ Plugin=WebView2 URL=file://#@#InformationProperty\index.html W=450 H=650 -X=25 -Y=25 +X=0 +Y=0 + ; ======================================== ; Background ; ======================================== [MeterBackground] Meter=Shape -Shape=Rectangle 0,0,500,700 | FillColor 0,0,0,2 | StrokeWidth 0 +Shape=Rectangle 0,0,450,650 | FillColor 0,0,0,2 | StrokeWidth 0 diff --git a/Resources/Skins/WebView2/IslamicDate/IslamicDate.ini b/Resources/Skins/WebView2/IslamicDate/IslamicDate.ini index 3aa32d9..11da74d 100644 --- a/Resources/Skins/WebView2/IslamicDate/IslamicDate.ini +++ b/Resources/Skins/WebView2/IslamicDate/IslamicDate.ini @@ -1,12 +1,16 @@ [Rainmeter] -Update=1000 +Update=-1 [Metadata] Name=Islamic Date Author=nstechbytes Information=Islamic (Hijri) Date Widget using WebView2 -Version=0.0.7 +Version=0.0.8 License=Creative Commons Attribution-Non-Commercial-Share Alike 3.0 + +[Variables] +Scale=1 + ; ======================================== ; Measure ; ======================================== @@ -14,14 +18,20 @@ License=Creative Commons Attribution-Non-Commercial-Share Alike 3.0 Measure=Plugin Plugin=WebView2 URL=#@#IslamicDate\index.html -W=280 -H=160 -X=15 -Y=15 +W=(280*#Scale#) +H=(160*#Scale#) +X=0 +Y=0 +Clickthrough=1 +ZoomFactor=#Scale# +DynamicVariables=1 + ; ======================================== ; Background ; ======================================== [Background_Shape] Meter=Shape -Shape=Rectangle 0,0,310,190 | StrokeWidth 0 | FillColor 0,0,0,1 - +Shape=Rectangle 0,0,(280*#Scale#),(160*#Scale#) | StrokeWidth 0 | FillColor 0,0,0,1 +MouseScrollUpAction=[!SetVariable Scale "(Clamp(#Scale#+0.1,0.5,2.5))"][!UpdateMeter #CURRENTSECTION#][!UpdateMeasure WebView2][!Redraw] +MouseScrollDownAction=[!SetVariable Scale "(Clamp(#Scale#-0.1,0.5,2.5))"][!UpdateMeter #CURRENTSECTION#][!UpdateMeasure WebView2][!Redraw] +DynamicVariables=1 diff --git a/Resources/Skins/WebView2/JSInteraction/JSInteraction.ini b/Resources/Skins/WebView2/JSInteraction/JSInteraction.ini index 3e1ba0b..98a147d 100644 --- a/Resources/Skins/WebView2/JSInteraction/JSInteraction.ini +++ b/Resources/Skins/WebView2/JSInteraction/JSInteraction.ini @@ -7,7 +7,7 @@ DynamicWindowSize=1 Name=JSInteraction Author=nstechbytes Information=Demonstrates JavaScript interaction (OnInitialize, OnUpdate, CallJS) with the WebView2 plugin. -Version=0.0.7 +Version=0.0.8 License=Creative Commons Attribution-Non-Commercial-Share Alike 3.0 ; ======================================== @@ -52,7 +52,6 @@ Text=JS Interaction Demo [MeterStatus] Meter=String -MeasureName=MeasureWebView X=225 Y=5R W=400 @@ -62,7 +61,6 @@ FontSize=10 FontColor=100,255,100,255 StringAlign=Center AntiAlias=1 -Text=Status: %1 [MeterCallJSResult] Meter=String diff --git a/Resources/Skins/WebView2/Permissions/Permissions.ini b/Resources/Skins/WebView2/Permissions/Permissions.ini new file mode 100644 index 0000000..0426b02 --- /dev/null +++ b/Resources/Skins/WebView2/Permissions/Permissions.ini @@ -0,0 +1,34 @@ +[Rainmeter] +Update=-1 +AccurateText=1 +DynamicWindowSize=1 + +[Metadata] +Name=Permissions +Author=RicardoTM +Information=For Permissions testing. +Version=0.0.8 +License=Creative Commons Attribution-Non-Commercial-Share Alike 3.0 + +[Variables] +DemoVariable=WebView2 Utility + +; ======================================== +; Measure +; ======================================== +[MeasureWebView] +Measure=Plugin +Plugin=WebView2 +URL=file://#@#Permissions\index.html +W=450 +H=650 +X=0 +Y=0 +AllowNotifications=0 + +; ======================================== +; Background +; ======================================== +[MeterBackground] +Meter=Shape +Shape=Rectangle 0,0,450,650 | FillColor 0,0,0,2 | StrokeWidth 0 diff --git a/Resources/Skins/WebView2/ReadMeasureOption/ReadMeasureOption.ini b/Resources/Skins/WebView2/ReadMeasureOption/ReadMeasureOption.ini index 6e2729c..973d45d 100644 --- a/Resources/Skins/WebView2/ReadMeasureOption/ReadMeasureOption.ini +++ b/Resources/Skins/WebView2/ReadMeasureOption/ReadMeasureOption.ini @@ -7,8 +7,9 @@ DynamicWindowSize=1 Name=ReadMeasureOption Author=nstechbytes Information=Demonstrates reading options from the current measure using the WebView2 plugin. -Version=0.0.7 +Version=0.0.8 License=Creative Commons Attribution-Non-Commercial-Share Alike 3.0 + ; ======================================== ; Measure ; ======================================== @@ -18,8 +19,8 @@ Plugin=WebView2 URL=file://#@#ReadMeasureOption\index.html W=400 H=600 -X=25 -Y=25 +X=0 +Y=0 ; Options to be read by the JavaScript TestString=Hello from Rainmeter! TestInt=1337 @@ -33,4 +34,4 @@ TestPath=ReadMeasureOption.ini [MeterBackground] Meter=Shape -Shape=Rectangle 0,0,450,650 | FillColor 0,0,0,2 | StrokeWidth 0 \ No newline at end of file +Shape=Rectangle 0,0,400,600 | FillColor 0,0,0,2 | StrokeWidth 0 \ No newline at end of file diff --git a/Resources/Skins/WebView2/ReadSectionOption/ReadSectionOption.ini b/Resources/Skins/WebView2/ReadSectionOption/ReadSectionOption.ini index 62d4c9e..213a72f 100644 --- a/Resources/Skins/WebView2/ReadSectionOption/ReadSectionOption.ini +++ b/Resources/Skins/WebView2/ReadSectionOption/ReadSectionOption.ini @@ -7,40 +7,41 @@ DynamicWindowSize=1 Name=ReadSectionOption Demo Author=nstechbytes Information=Demonstrates reading options from other sections using WebView2 -Version=0.0.7 +Version=0.0.8 License=Creative Commons Attribution-Non-Commercial-Share Alike 3.0 [Variables] TestVar=Hello from Variables! DoubleExample=25.5 + ; ================================================== ; Measures to Read ; ================================================== - [MeasureTest] Measure=String String=This is a string measure [MeasureCalc] Measure=Calc -Formula=25 +Formula=MeasureCalc + 1 +DynamicVariables=1 + ; ================================================== ; WebView2 Plugin ; ================================================== - [MeasureWebView] Measure=Plugin Plugin=WebView2 URL=#@#ReadSectionOption\index.html W=400 H=600 -X=25 -Y=25 +X=0 +Y=0 +DynamicVariables=1 ; ================================================== ; Background ; ================================================== - [MeterBackground] Meter=Shape -Shape=Rectangle 0,0,450,650 | FillColor 0,0,0,2 | StrokeWidth 0 +Shape=Rectangle 0,0,400,600 | FillColor 0,0,0,2 | StrokeWidth 0 diff --git a/Resources/Skins/WebView2/UtilityFunction/UtilityFunction.ini b/Resources/Skins/WebView2/UtilityFunction/UtilityFunction.ini index 79d64f3..a62912a 100644 --- a/Resources/Skins/WebView2/UtilityFunction/UtilityFunction.ini +++ b/Resources/Skins/WebView2/UtilityFunction/UtilityFunction.ini @@ -7,11 +7,12 @@ DynamicWindowSize=1 Name=UtilityFunction Author=nstechbytes Information=Demonstrates RainmeterAPI utility functions using the WebView2 plugin. -Version=0.0.7 +Version=0.0.8 License=Creative Commons Attribution-Non-Commercial-Share Alike 3.0 [Variables] DemoVariable=WebView2 Utility + ; ======================================== ; Measure ; ======================================== @@ -21,11 +22,12 @@ Plugin=WebView2 URL=file://#@#UtilityFunction\index.html W=450 H=650 -X=25 -Y=25 +X=0 +Y=0 + ; ======================================== ; Background ; ======================================== [MeterBackground] Meter=Shape -Shape=Rectangle 0,0,500,700 | FillColor 0,0,0,2 | StrokeWidth 0 +Shape=Rectangle 0,0,450,650 | FillColor 0,0,0,2 | StrokeWidth 0 diff --git a/Resources/Skins/WebView2/Weather/Weather.ini b/Resources/Skins/WebView2/Weather/Weather.ini index 66e0189..1b2ca62 100644 --- a/Resources/Skins/WebView2/Weather/Weather.ini +++ b/Resources/Skins/WebView2/Weather/Weather.ini @@ -1,12 +1,22 @@ [Rainmeter] -Update=1000 +Update=-1 +ContextTitle=Open DevTools +ContextAction=[!CommandMeasure WebView2 "Open DevTools"] +ContextTitle2= Open Task Manager +ContextAction2=[!CommandMeasure WebView2 "Open TaskManager"] +ContextTitle3= Reload +ContextAction3=[!CommandMeasure WebView2 "Navigate Reload"] [Metadata] Name=Weather Author=nstechbytes Information=Weather Widget using WebView2 -Version=0.0.7 +Version=0.0.8 License=Creative Commons Attribution-Non-Commercial-Share Alike 3.0 + +[Variables] +Scale=1 + ; ======================================== ; Measure ; ======================================== @@ -14,14 +24,20 @@ License=Creative Commons Attribution-Non-Commercial-Share Alike 3.0 Measure=Plugin Plugin=WebView2 URL=#@#Weather\index.html -W=280 -H=350 -X=15 -Y=15 +W=(280*#Scale#) +H=(300*#Scale#) +X=0 +Y=0 +ZoomFactor=#Scale# +Clickthrough=1 +DynamicVariables=1 + ; ======================================== ; Background ; ======================================== [Background_Shape] Meter=Shape -Shape=Rectangle 0,0,310,400 | StrokeWidth 0 | FillColor 0,0,0,1 - +Shape=Rectangle 0,0,(280*#Scale#),(300*#Scale#) | StrokeWidth 0 | FillColor 0,0,0,1 +MouseScrollUpAction=[!SetVariable Scale "(Clamp(#Scale#+0.1,0.5,2.5))"][!UpdateMeter #CURRENTSECTION#][!UpdateMeasure WebView2][!Redraw] +MouseScrollDownAction=[!SetVariable Scale "(Clamp(#Scale#-0.1,0.5,2.5))"][!UpdateMeter #CURRENTSECTION#][!UpdateMeasure WebView2][!Redraw] +DynamicVariables=1 diff --git a/Resources/Skins/WebView2/YoutubePlayer/YoutubePlayer.ini b/Resources/Skins/WebView2/YoutubePlayer/YoutubePlayer.ini new file mode 100644 index 0000000..01160a5 --- /dev/null +++ b/Resources/Skins/WebView2/YoutubePlayer/YoutubePlayer.ini @@ -0,0 +1,127 @@ +[Rainmeter] +Update=-1 +; ============================ +; Context menu +; ============================ +ContextTitle=Open DevTools +ContextAction=[!CommandMeasure WebView2 "Open DevTools"] + +ContextTitle2=Open Task Manager +ContextAction2=[!CommandMeasure WebView2 "Open TaskManager"] + +ContextTitle3=Reload +ContextAction3=[!CommandMeasure WebView2 "Navigate Reload"] + +; Persist scale value when the skin is closed +OnCloseAction=[!WriteKeyValue Variables Scale [#Scale]] + +[Metadata] +Name=Weather +Author=RicardoTM +Information=Simple YouTube Player +Version=0.0.8 +License=Creative Commons Attribution-Non-Commercial-Share Alike 3.0 + +; ============================================================================= +; About virtual hosts +; ============================================================================= +; This skin demonstrates the use of a WebView2 virtual host. +; +; Using a virtual host allows loading resources that are restricted when using +; the local file:// protocol. +; +; By serving the page through http/https instead of file://: +; - YouTube embeds work correctly (avoids Error 153) +; - Autoplay and other browser APIs are available +; - Local storage can be scoped to the skin +; +; In this case, the WebView2 plugin automatically creates and navigates to: +; https://webview2-youtubeplayer/index.html +; ============================================================================= + +[Variables] +; UI scale multiplier (mouse wheel adjustable) +Scale=1 +; Base width and height +W=558 +H=361 +; Colors +BGColor=0f0f0f +TxtColor=ffffff + +; ======================================== +; WebView2 Measure +; ======================================== +[WebView2] +Measure=Plugin +Plugin=WebView2 + +; HostSecurity: +; 0 = http (insecure context) +; 1 = https (secure context) +; HTTPS is required for YouTube autoplay and modern web APIs +HostSecurity=1 + +; HostOrigin: +; 1 = root config +; 0 = current config only +; Using current config isolates storage to this skin +HostOrigin=0 + +; Folder containing the HTML and assets +HostPath=#@#YoutubePlayer\ + +; Entry HTML file +URL=index.html + +; Make it scalable +W=(#W#*#Scale#) +H=(#H#*#Scale#) + +; Allow CTRL + drag to move skin and open skin menu +Clickthrough=2 + +; Allow links to open in external windows +NewWindow=1 + +; Disable accessibility features (caret browsing, find and print) +AssistiveFeatures=0 + +; Show loading meter when navigation starts +OnPageLoadStartAction=[!UpdateMeasure #CURRENTSECTION#][!ShowMeter Loading][!UpdateMeter Loading][!Redraw] + +; Hide loading meter when navigation finishes +OnPageLoadFinishAction=[!UpdateMeasure #CURRENTSECTION#][!HideMeter Loading][!UpdateMeter Loading][!Redraw] +DynamicVariables=1 + +; ======================================== +; Background +; ======================================== +[Background_Shape] +Meter=Shape +Shape=Rectangle 0,0,(#W#*#Scale#),(#H#*#Scale#),16 | StrokeWidth 0 | FillColor #BGColor# + +; Mouse wheel scaling (requires holding CTRL) +MouseScrollUpAction=[!SetVariable Scale "(Clamp(#Scale#+0.1,0.6,2.5))"][!Update] +MouseScrollDownAction=[!SetVariable Scale "(Clamp(#Scale#-0.1,0.6,2.5))"][!Update] + +; Reset scale with middle click (requires holding CTRL) +MiddleMouseUpAction=[!SetVariable Scale 1][!Update] +DynamicVariables=1 + +; ======================================== +; Loading indicator +; ======================================== +; Displayed while the WebView2 page is loading. +; Automatically shown and hidden by OnPageLoadStartAction and OnPageLoadFinishAction. +[Loading] +Hidden=1 +Meter=String +Text=Loading... +FontSize=12 +AntiAlias=1 +FontColor=#TxtColor# +StringAlign=CenterCenter +X=((#W#/2)*#Scale#) +Y=((#H#/2)*#Scale#) +DynamicVariables=1 diff --git a/Resources/skin_definition.json b/Resources/skin_definition.json index d8ceb85..19bfaae 100644 --- a/Resources/skin_definition.json +++ b/Resources/skin_definition.json @@ -1,6 +1,6 @@ { "skinDir": ".\\Resources\\Skins", - "version": "0.0.7", + "version": "0.0.8", "minimumVersion": "4.5", "author": "nstechbytes", "variableFiles": "", @@ -17,5 +17,5 @@ "load": "WebView2\\Clock\\Clock.ini", "headerImage": ".\\Resources\\banner.bmp", "configPrefix": "WebView2", - "output": ".\\dist\\WebView2_v0.0.7_Alpha7.rmskin" + "output": ".\\dist\\WebView2_v0.0.8_Alpha8.rmskin" } diff --git a/WebView2/Plugin.cpp b/WebView2/Plugin.cpp index dbab4cd..54c957b 100644 --- a/WebView2/Plugin.cpp +++ b/WebView2/Plugin.cpp @@ -1,7 +1,12 @@ // Copyright (C) 2025 nstechbytes. All rights reserved. #include "Plugin.h" #include "../API/RainmeterAPI.h" +#include +#include +#include +#include +#pragma comment(lib, "comctl32.lib") #pragma comment(lib, "ole32.lib") #pragma comment(lib, "oleaut32.lib") @@ -11,58 +16,76 @@ static bool g_comInitialized = false; // Global TypeLib for COM objects wil::com_ptr g_typeLib; +// Global keyboard hook and state +static HMODULE g_hModule = nullptr; +HHOOK g_kbHook = nullptr; +std::atomic g_ctrlDown{ false }; +std::atomic g_refCount(0); +static std::atomic g_hookAlive{ false }; + +static std::mutex g_skinMapMutex; + +std::wstring ToLower(std::wstring s) +{ + if (!s.empty()) + { + CharLowerBuffW(s.data(), static_cast(s.size())); + } + return s; +} + // 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), +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 @@ -70,500 +93,872 @@ Measure::~Measure() { } -// Rainmeter Plugin Exports -PLUGIN_EXPORT void Initialize(void** data, void* rm) +// Helper Functions +void ShowFailure(HRESULT hr, const std::wstring& message) { - Measure* measure = new Measure; - *data = measure; - - measure->rm = rm; - measure->skin = RmGetSkin(rm); - measure->skinWindow = RmGetSkinWindow(rm); - measure->measureName = RmGetMeasureName(rm); + LPWSTR systemMessage = nullptr; + + FormatMessageW( + FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, + hr, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&systemMessage), + 0, + nullptr); + + std::wstringstream formattedMessage; + formattedMessage << message + << L": 0x" + << std::hex << std::setw(8) << std::setfill(L'0') + << hr; + + if (systemMessage) + { + formattedMessage << L" (" << systemMessage << L")"; + LocalFree(systemMessage); + } + + MessageBoxW(nullptr, formattedMessage.str().c_str(), nullptr, MB_OK); } -// Helper to update clickthrough state -void UpdateClickthrough(Measure* measure) +void CheckFailure(HRESULT hr, const std::wstring& message) { - 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 (FAILED(hr)) + { + ShowFailure(hr, message); + FAIL_FAST(); + } } -// Inject AllowDualControl script into the WebView -void InjectAllowDualControl(Measure* measure) +void FeatureNotAvailable() { - 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); + MessageBox(nullptr, + L"This feature is not available in the WebView2 runtime version currently being used.", + L"Feature Not Available", MB_OK); } -// Update AllowDualControl state in the WebView -void UpdateAllowDualControl(Measure* measure) +void UpdateWindowBounds(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 || !measure->webViewController) + return; + + RECT bounds{ + measure->x, + measure->y, + measure->x + measure->width, + measure->y + measure->height + }; + + measure->webViewController->put_Bounds(bounds); + measure->webViewController->NotifyParentWindowPositionChanged(); } -PLUGIN_EXPORT void Reload(void* data, void* rm, double* maxValue) +void UpdateChildWindowState(Measure* measure, bool enabled, bool shouldDefocus) { - 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; - - // Read AllowDualControl for Yincognito's script injection - bool newAllowDualControl = RmReadInt(rm, L"AllowDualControl", 1) >= 1; + if (!measure || measure->isStopping || !measure->skinWindow || !IsWindow(measure->skinWindow)) + return; - // Read OnWebViewLoadAction - std::wstring newOnWebViewLoadAction; - LPCWSTR onWebViewLoadOption = RmReadString(rm, L"OnWebViewLoadAction", L"", FALSE); - if (onWebViewLoadOption && wcslen(onWebViewLoadOption) > 0) + for (HWND child = GetWindow(measure->skinWindow, GW_CHILD); + child != nullptr; + child = GetWindow(child, GW_HWNDNEXT)) { - newOnWebViewLoadAction = onWebViewLoadOption; + if (!IsWindow(child)) + continue; + + EnableWindow(child, enabled); + + if (!enabled && shouldDefocus) + { + HWND focused = GetFocus(); + if (focused && IsWindow(focused) && + (focused == child || IsChild(child, focused))) + { + SetFocus(nullptr); + } + } } +} - // Read OnWebViewFailAction - std::wstring newOnWebViewFailAction; - LPCWSTR onWebViewFailOption = RmReadString(rm, L"OnWebViewFailAction", L"", FALSE); - if (onWebViewFailOption && wcslen(onWebViewFailOption) > 0) +std::wstring GetHostName(const std::wstring& input, bool origin) +{ + std::wstring result; + result.reserve(input.size()); + + for (wchar_t ch : input) { - newOnWebViewFailAction = onWebViewFailOption; + if (origin && (ch == L'/' || ch == L'\\')) + { + break; // stop processing at first '/' + } + + if ((ch >= L'a' && ch <= L'z') || + (ch >= L'A' && ch <= L'Z') || + (ch >= L'0' && ch <= L'9')) + { + result.push_back(ch); + } + else + { + result.push_back(L'-'); + } } - // Read OnPageLoadStartAction - std::wstring newOnPageLoadStartAction; - LPCWSTR onPageLoadStartOption = RmReadString(rm, L"OnPageLoadStartAction", L"", FALSE); - if (onPageLoadStartOption && wcslen(onPageLoadStartOption) > 0) + // convert to lowercase + result = ToLower(result); + + return result; +} + +// Skin subclass procedure +LRESULT CALLBACK SkinSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) +{ + SkinSubclassData* skinData = + reinterpret_cast(dwRefData); + + if (!skinData || skinData->destroying) + return DefSubclassProc(hWnd, uMsg, wParam, lParam); + switch (uMsg) + { + case WM_APP_CTRL_CHANGED: { - newOnPageLoadStartAction = onPageLoadStartOption; + bool isCtrlPressed = (bool)wParam; + + for (Measure* measure : skinData->measures) + { + if (!measure || !measure->initialized || !measure->visible) + continue; + + measure->isCtrlPressed = isCtrlPressed; + + if (measure->clickthrough <= 1) + continue; + + if (measure->clickthrough >= 2) + { + if (measure->isClickthroughActive != isCtrlPressed) + { + measure->isClickthroughActive = isCtrlPressed; + + UpdateChildWindowState(measure, !isCtrlPressed, false); // Enable clickthrough if ctrl is pressed + } + } + } + return 0; } + case WM_MOVE: + case WM_MOVING: // Update bounds during drag so window.screenX/Y work correctly on JS. + for (Measure* measure : skinData->measures) + { + if (measure && measure->initialized) + { + UpdateWindowBounds(measure); + } + } + break; + case WM_DESTROY: + case WM_NCDESTROY: + RemoveWindowSubclass(hWnd, SkinSubclassProc, uIdSubclass); + break; + } + + return DefSubclassProc(hWnd, uMsg, wParam, lParam); +} - // Read OnPageLoadingAction - std::wstring newOnPageLoadingAction; - LPCWSTR onPageLoadingOption = RmReadString(rm, L"OnPageLoadingAction", L"", FALSE); - if (onPageLoadingOption && wcslen(onPageLoadingOption) > 0) +// Keyboard hook procedure +LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) +{ + if (!g_hookAlive.load(std::memory_order_acquire)) { - newOnPageLoadingAction = onPageLoadingOption; + return CallNextHookEx(nullptr, nCode, wParam, lParam); } - // Read OnPageLoadFinishAction - std::wstring newOnPageLoadFinishAction; - LPCWSTR onPageLoadFinishOption = RmReadString(rm, L"OnPageLoadFinishAction", L"", FALSE); - if (onPageLoadFinishOption && wcslen(onPageLoadFinishOption) > 0) + if (nCode == HC_ACTION) { - newOnPageLoadFinishAction = onPageLoadFinishOption; + auto* kb = reinterpret_cast(lParam); + + if (kb->vkCode == VK_LCONTROL || kb->vkCode == VK_RCONTROL) + { + bool down = (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN); + bool old = g_ctrlDown.exchange(down, std::memory_order_relaxed); + + if (down != old) + { + // Copy HWNDs under lock, then release lock and post messages. + std::vector targets; + { + std::lock_guard lg(g_skinMapMutex); + targets.reserve(g_SkinSubclassMap.size()); + for (const auto& p : g_SkinSubclassMap) + targets.push_back(p.first); + } + + // PostMessage without holding the map lock. Check IsWindow to avoid posting to invalid HWND. + for (HWND hwnd : targets) + { + if (IsWindow(hwnd)) + { + PostMessage(hwnd, WM_APP_CTRL_CHANGED, static_cast(down), 0); + } + } + } + } } - // Read OnPageFirstLoadAction - std::wstring newOnPageFirstLoadAction; - LPCWSTR onPageFirstLoadOption = RmReadString(rm, L"OnPageFirstLoadAction", L"", FALSE); - if (onPageFirstLoadOption && wcslen(onPageFirstLoadOption) > 0) + return CallNextHookEx(g_kbHook, nCode, wParam, lParam); +} + +void InstallKeyboardHook() +{ + if (!g_hModule) { - newOnPageFirstLoadAction = onPageFirstLoadOption; + GetModuleHandleEx( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, + reinterpret_cast(&LowLevelKeyboardProc), + &g_hModule + ); } + if (!g_kbHook && g_hModule) + { + g_hookAlive.store(true, std::memory_order_release); + g_kbHook = SetWindowsHookEx( + WH_KEYBOARD_LL, + LowLevelKeyboardProc, + g_hModule, + 0 + ); + } +} - // Read OnPageReloadAction - std::wstring newOnPageReloadAction; - LPCWSTR onPageReloadOption = RmReadString(rm, L"OnPageReloadAction", L"", FALSE); - if (onPageReloadOption && wcslen(onPageReloadOption) > 0) +void RemoveKeyboardHook() +{ + if (g_kbHook) { - newOnPageReloadAction = onPageReloadOption; + g_hookAlive.store(false, std::memory_order_release); + UnhookWindowsHookEx(g_kbHook); + g_kbHook = nullptr; } - - // 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; +} + +// Rainmeter Plugin Exports +PLUGIN_EXPORT void Initialize(void** data, void* rm) +{ + Measure* measure = new Measure; + *data = measure; + + if (g_refCount++ == 0) + { + InstallKeyboardHook(); + } + + bool ctrlDown = g_ctrlDown.load(std::memory_order_relaxed); + measure->isCtrlPressed = ctrlDown; + + measure->rm = rm; + measure->skin = RmGetSkin(rm); + measure->skinWindow = RmGetSkinWindow(rm); + measure->measureName = RmGetMeasureName(rm); + measure->skinName = RmGetSkinName(rm); + measure->hostSecurity = RmReadInt(rm, L"HostSecurity", 1) >= 1; + measure->hostOrigin = RmReadInt(rm, L"HostOrigin", 1) >= 1; + + measure->hostName = GetHostName(measure->skinName, measure->hostOrigin); + + GetUserDefaultLocaleName(measure->osLocale, LOCALE_NAME_MAX_LENGTH); + + if (!measure->skinWindow) + return; + + HRESULT hr = GetAvailableCoreWebView2BrowserVersionString(nullptr, &measure->runtimeVersion); + if (SUCCEEDED(hr) && measure->runtimeVersion != nullptr) + { + measure->isRuntimeInstalled = true; + } + else + { + int result = MessageBox( + measure->skinWindow, + L"WebView2 Runtime is not installed.\n\n" + L"Would you like to be redirected to the WebView2 Runtime download page?", + L"WebView2 Runtime Error", + MB_ICONERROR | MB_YESNO + ); + + if (result == IDYES) + { + ShellExecuteW( + nullptr, + L"open", + L"https://developer.microsoft.com/microsoft-edge/webview2/", + nullptr, + nullptr, + SW_SHOWNORMAL + ); + } + measure->FailWebView(hr, L"WebView2: WebView2 Runtime is not installed."); + measure->isRuntimeInstalled = false; + return; + } + + measure->autoStart = RmReadInt(rm, L"AutoStart", 1) >= 1; + + // Create user data folder in TEMP directory to avoid permission issues + wchar_t tempPath[MAX_PATH]; + GetTempPathW(MAX_PATH, tempPath); + measure->userDataFolder = std::wstring(tempPath) + L"RainmeterWebView2"; + measure->configPath = measure->userDataFolder + L"\\WebView2Settings.ini"; + + // Create the directory if it doesn't exist + CreateDirectoryW(measure->userDataFolder.c_str(), nullptr); + + SkinSubclassData* skinData = nullptr; + bool createdNew = false; + + { + std::lock_guard lg(g_skinMapMutex); + auto it = g_SkinSubclassMap.find(measure->skinWindow); + if (it == g_SkinSubclassMap.end()) + { + skinData = new SkinSubclassData; + skinData->hwnd = measure->skinWindow; + skinData->refCount = 1; + g_SkinSubclassMap[measure->skinWindow] = skinData; + createdNew = true; + } + else + { + skinData = it->second; + skinData->refCount++; + } + } + + // Set subclass if this is the first measure for the skin + if (createdNew) + { + SetWindowSubclass( + measure->skinWindow, + SkinSubclassProc, + 1, + reinterpret_cast(skinData) + ); + } + + // Register this measure with the skin + skinData->measures.insert(measure); + measure->skinData = skinData; +} + +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->isRuntimeInstalled || measure->disabled) + { + return; + } + + // 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 int newClickthrough = RmReadInt(rm, L"Clickthrough", 2); + const double newZoomFactor = RmReadFormula(rm, L"ZoomFactor", 1.0); + const bool newVisible = RmReadInt(rm, L"Hidden", 0) <= 0; + const bool newNotifications = RmReadInt(rm, L"Notifications", 0) >= 1; + const bool newNewWindow = RmReadInt(rm, L"NewWindow", 0) >= 1; + const bool newZoomControl = RmReadInt(rm, L"ZoomControl", 1) >= 1; + const bool newAssistiveFeatures = RmReadInt(rm, L"AssistiveFeatures", 1) >= 1; + const bool newHostSecurity = RmReadInt(rm, L"HostSecurity", 1) >= 1; + const bool newHostOrigin = RmReadInt(rm, L"HostOrigin", 1) >= 1; + const std::wstring newHostPath = RmReadString(rm, L"HostPath", L""); + const std::wstring newUserAgent = RmReadString(rm, L"UserAgent", L""); + + // URL handling + std::wstring newUrl; + LPCWSTR urlOption = RmReadString(rm, L"Url", L""); + + if (urlOption && *urlOption) + { + 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 (newHostPath.empty()) // Map to file:/// + { + 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; + } + } + else // Map to virtual host + { + const std::wstring protocol = newHostSecurity ? L"https://" : L"http://"; + if (urlStr[0] != L'\\') + { + urlStr = protocol + std::wstring(measure->hostName.c_str()) + L"/" + urlStr; + } + else + { + urlStr = protocol + std::wstring(measure->hostName.c_str()) + urlStr; + } + } + } + + newUrl = urlStr; + } + } + + // 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 dimensionsChanged = (newWidth != measure->width || newHeight != measure->height || newX != measure->x || newY != measure->y); + const bool visibilityChanged = (newVisible != measure->visible); + const bool zoomFactorChanged = (newZoomFactor != measure->zoomFactor); + const bool zoomControlChanged = (newZoomControl != measure->zoomControl); + const bool userAgentChanged = (newUserAgent != measure->userAgent); + const bool clickthroughChanged = (newClickthrough != measure->clickthrough); + const bool hostChanged = (newHostPath != measure->hostPath || newHostSecurity != measure->hostSecurity || newHostOrigin != measure->hostOrigin); + + // 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->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 (clickthroughChanged) - { - UpdateClickthrough(measure); - } - - if (allowDualControlChanged) - { - if (!measure->isAllowDualControlInjected) - { - InjectAllowDualControl(measure); - } + measure->zoomControl = newZoomControl; + measure->visible = newVisible; + measure->clickthrough = newClickthrough; + measure->notifications = newNotifications; + measure->newWindow = newNewWindow; + measure->hostPath = newHostPath; + measure->userAgent = newUserAgent; + measure->assistiveFeatures = newAssistiveFeatures; + + // 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) + { + if (measure->isCreationInProgress) + { + return; + } + + CreateWebView2(measure); + measure->autoStart = false; + return; + } + + // Dynamic updates + if (clickthroughChanged) + { + measure->isClickthroughActive = false; + if (measure->clickthrough <= 0 || measure->clickthrough == 2) + { + UpdateChildWindowState(measure, true); + } + else if (measure->clickthrough == 1) + { + UpdateChildWindowState(measure, false); + } + } + + // Controller Options + if (dimensionsChanged && measure->webViewController) + { + UpdateWindowBounds(measure); + } + + if (zoomFactorChanged && measure->webViewController) + { + measure->webViewController->put_ZoomFactor(measure->zoomFactor); + } + + if (hostChanged) + { + if (measure->webView3) + { + if (!measure->hostPath.empty()) + { + if (newHostSecurity != measure->hostSecurity || newHostOrigin != measure->hostOrigin) + { + measure->hostName = GetHostName(measure->skinName, measure->hostOrigin); + } + measure->webView3->SetVirtualHostNameToFolderMapping(measure->hostName.c_str(), measure->hostPath.c_str(), COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_ALLOW); + } else - UpdateAllowDualControl(measure); - } - } + { + measure->webView3->ClearVirtualHostNameToFolderMapping(measure->hostName.c_str()); + } + } + } + + if (visibilityChanged && measure->webViewController) + { + measure->webViewController->put_IsVisible(measure->visible); + + if (!measure->visible) { + + SetFocus(nullptr); + + if (measure->webView3) { + // Suspend to save resources + measure->webView3->TrySuspend(Callback( + [](HRESULT errorCode, BOOL isSuccessful) -> HRESULT { + return S_OK; + }) + .Get()); + } + } + else if (measure->webView3) + { + // Resume when made visible + measure->webView3->Resume(); + } + } + + // Core Options + if (zoomControlChanged && measure->webViewSettings) + { + measure->webViewSettings->put_IsZoomControlEnabled(measure->zoomControl); + } + + if (userAgentChanged && measure->webViewSettings2) + { + measure->webViewSettings2->put_UserAgent(measure->userAgent.c_str()); + } + } 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 + { + 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 current url if available, otherwise return "" + if (!measure->currentUrl.empty()) + { + return measure->currentUrl.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; + } + + if (!measure->isRuntimeInstalled) + { + RmLog(measure->rm, LOG_ERROR, L"WebView2: WebView2 Runtime is not installed."); + 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"WebView") == 0) + { + if (_wcsicmp(param.c_str(), L"Start") == 0) + { + CreateWebView2(measure); + return; + } + else if (_wcsicmp(param.c_str(), L"Stop") == 0) + { + StopWebView2(measure); + return; + } + else if (_wcsicmp(param.c_str(), L"Restart") == 0) + { + RestartWebView2(measure); + return; + } + else + { + RmLog(measure->rm, LOG_ERROR, L"WebView2: Unknown WebView command"); + return; + } + } + + if (!measure->webView) + { + RmLog(measure->rm, LOG_ERROR, L"WebView2: Not running"); + return; + } + + // Navigation Commands + if (_wcsicmp(action.c_str(), L"Navigate") == 0) + { + if (_wcsicmp(param.c_str(), L"Stop") == 0) + { + measure->webView->Stop(); + } + else if (_wcsicmp(param.c_str(), L"Reload") == 0) + { + measure->webView->Reload(); + } + else if (_wcsicmp(param.c_str(), L"Back") == 0) + { + measure->webView->GoBack(); + } + else if (_wcsicmp(param.c_str(), L"Forward") == 0) + { + measure->webView->GoForward(); + } + else if (_wcsicmp(param.c_str(), L"Home") == 0) + { + measure->webView->Navigate(measure->url.c_str()); + } + else + { + // Navigate to URL + measure->webView->Navigate(param.c_str()); + } + } + else if (_wcsicmp(action.c_str(), L"Open") == 0) + { + if (_wcsicmp(param.c_str(), L"DevTools") == 0) + { + measure->webView->OpenDevToolsWindow(); + + } + else if (_wcsicmp(param.c_str(), L"TaskManager") == 0) + { + measure->webView6->OpenTaskManagerWindow(); + } + } + 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 + { + RmLogF(measure->rm, LOG_ERROR, L"WebView2: Unknown command - %s", action.c_str()); + } } // 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() + ); + + // 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; + if (!measure) return; + SkinSubclassData* skinData = measure->skinData; + + SkinSubclassData* toDelete = nullptr; + + if (skinData) + { + // Modify map + decide deletion under lock + std::lock_guard lg(g_skinMapMutex); + + skinData->destroying = true; + skinData->measures.erase(measure); + skinData->refCount--; + + // If no more measures for this skin, mark for deletion + if (skinData->refCount <= 0) + { + g_SkinSubclassMap.erase(skinData->hwnd); + toDelete = skinData; + } + } + + // Remove subclass and delete outside lock + if (toDelete) + { + RemoveWindowSubclass( + toDelete->hwnd, + SkinSubclassProc, + 1 + ); + delete toDelete; + } + + // Stop WebView2 and clean up + if (measure->initialized) StopWebView2(measure); + + g_refCount--; + + // Remove keyboard hook if no more measures exist + if (g_refCount == 0) + { + RemoveKeyboardHook(); + } + + delete measure; +} \ No newline at end of file diff --git a/WebView2/Plugin.h b/WebView2/Plugin.h index c51e5d6..9850e06 100644 --- a/WebView2/Plugin.h +++ b/WebView2/Plugin.h @@ -2,68 +2,152 @@ #pragma once #include +#include +#include "SimpleIni.h" +#include +#include #include +#include #include -#include -#include -#include +#include +#include +#include +#include +#include using namespace Microsoft::WRL; // Global TypeLib for COM objects extern wil::com_ptr g_typeLib; +#define WM_APP_CTRL_CHANGED (WM_APP + 100) // Custom message for Ctrl key state change + +// Structure to hold frame information +struct Frames +{ + wil::com_ptr frame; + bool injected = false; + bool isDestroyed = false; +}; + +struct SkinSubclassData; + // Measure structure containing WebView2 state struct Measure { - void* rm; - void* skin; - HWND skinWindow; - LPCWSTR measureName; - - std::wstring url; - std::wstring currentUrl; - int width; - int height; - int x; - int y; - double zoomFactor; - bool visible; - bool initialized; - bool clickthrough; - bool isCreationInProgress = false; - bool isFirstLoad = true; - bool allowDualControl; - bool isAllowDualControlInjected = false; - - std::wstring onWebViewLoadAction; - std::wstring onWebViewFailAction; - std::wstring onPageFirstLoadAction; - std::wstring onPageLoadStartAction; - std::wstring onPageLoadingAction; - std::wstring onPageLoadFinishAction; - std::wstring onPageReloadAction; - - wil::com_ptr webViewController; - wil::com_ptr webView; - EventRegistrationToken webMessageToken; - - std::wstring buffer; // Buffer for section variable return values - std::wstring callbackResult; // Stores return value from OnInitialize/OnUpdate callbacks - std::map jsResults; // Cache for CallJS results - - Measure(); - ~Measure(); - - // Member callback functions for WebView2 creation - HRESULT CreateEnvironmentHandler(HRESULT result, ICoreWebView2Environment* env); - HRESULT CreateControllerHandler(HRESULT result, ICoreWebView2Controller* controller); + void* rm; + void* skin; + HWND skinWindow; + LPCWSTR measureName; + LPCWSTR skinName; + SkinSubclassData* skinData = nullptr; + + wchar_t osLocale[LOCALE_NAME_MAX_LENGTH] = { 0 }; + + std::wstring userDataFolder; + std::wstring configPath; + std::wstring url; + std::wstring currentUrl; + std::wstring currentTitle; + std::wstring hostName; + std::wstring hostPath; + std::wstring userAgent; + + int width; + int height; + int x; + int y; + int clickthrough = 1; + double zoomFactor = 1.0; + bool disabled = false; + bool autoStart = true; + bool visible = true; + bool initialized = false; + bool isFirstLoad = true; + bool isClickthroughActive = false; + bool notifications = false; + bool zoomControl = true; + bool newWindow = false; + bool isViewSource = false; + bool assistiveFeatures = true; + bool hostSecurity = true; + bool hostOrigin = true; + + bool isCreationInProgress = false; + bool isStopping = false; + bool isCtrlPressed = 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; + + CSimpleIniW ini; + bool iniDirty = false; + + wil::com_ptr webViewEnvironment; + wil::com_ptr webViewController; + wil::com_ptrwebViewControllerOptions2; + wil::com_ptr webView; + wil::com_ptr webView3; + wil::com_ptr webView6; + wil::com_ptr webViewProfile7; + wil::com_ptr webViewSettings; + wil::com_ptr webViewSettings2; + std::vector> Measure::webViewFrames; + + EventRegistrationToken webMessageToken; + + std::wstring buffer; // Buffer for section variable return values + std::map jsResults; // Cache for CallJS results + bool isRuntimeInstalled = false; + int state = -1; // Integer number to show the internal state of WebView and Navigation + wil::unique_cotaskmem_string runtimeVersion = nullptr; + + Measure(); + ~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 UpdateClickthrough(Measure* measure); -void InjectAllowDualControl(Measure* measure); -void UpdateAllowDualControl(Measure* measure); +void StopWebView2(Measure* measure); +void RestartWebView2(Measure* measure); +void UpdateChildWindowState(Measure* measure, bool enabled, bool shouldDefocus = true); + +// Taken from: https://github.com/MicrosoftEdge/WebView2Samples/blob/main/SampleApps/WebView2APISample/CheckFailure.h +// Notify the user of a failure with a message box. +void ShowFailure(HRESULT hr, const std::wstring& message = L"Error"); +// If something failed, show the error code and fail fast. +void CheckFailure(HRESULT hr, const std::wstring& message = L"Error"); +// Notify the user that a feature is not available +void FeatureNotAvailable(); + +#define CHECK_FAILURE_STRINGIFY(arg) #arg +#define CHECK_FAILURE_FILE_LINE(file, line) ([](HRESULT hr){ CheckFailure(hr, L"Failure at " CHECK_FAILURE_STRINGIFY(file) L"(" CHECK_FAILURE_STRINGIFY(line) L")"); }) +#define CHECK_FAILURE CHECK_FAILURE_FILE_LINE(__FILE__, __LINE__) +#define CHECK_FAILURE_BOOL(value) CHECK_FAILURE((value) ? S_OK : E_UNEXPECTED) +// Data structure for skin subclassing +struct SkinSubclassData +{ + HWND hwnd = nullptr; + int refCount = 0; + bool destroying = false; + + std::unordered_set measures = {}; +}; +static std::unordered_map g_SkinSubclassMap; \ No newline at end of file diff --git a/WebView2/SimpleIni.h b/WebView2/SimpleIni.h new file mode 100644 index 0000000..34d179e --- /dev/null +++ b/WebView2/SimpleIni.h @@ -0,0 +1,3666 @@ +/** @mainpage + + +
Library SimpleIni +
File SimpleIni.h +
Author Brodie Thiesfield +
Source https://github.com/brofield/simpleini +
Version 4.25 +
+ + Jump to the @link CSimpleIniTempl CSimpleIni @endlink interface documentation. + + @section intro INTRODUCTION + + This component allows an INI-style configuration file to be used on both + Windows and Linux/Unix. It is fast, simple and source code using this + component will compile unchanged on either OS. + + @section features FEATURES + + - MIT Licence allows free use in all software (including GPL and commercial) + - multi-platform (Windows CE/9x/NT..10/etc, Linux, MacOSX, Unix) + - loading and saving of INI-style configuration files + - configuration files can have any newline format on all platforms + - liberal acceptance of file format + - key/values with no section + - removal of whitespace around sections, keys and values + - support for multi-line values (values with embedded newline characters) + - optional support for multiple keys with the same name + - optional case-insensitive sections and keys (for ASCII characters only) + - saves files with sections and keys in the same order as they were loaded + - preserves comments on the file, section and keys where possible. + - supports both char or wchar_t programming interfaces + - supports both MBCS (system locale) and UTF-8 file encodings + - system locale does not need to be UTF-8 on Linux/Unix to load UTF-8 file + - support for non-ASCII characters in section, keys, values and comments + - support for non-standard character types or file encodings + via user-written converter classes + - support for adding/modifying values programmatically + - should compile cleanly without warning usually at the strictest warning level + - it has been tested with the following compilers: + - Windows/VC6 (warning level 3) + - Windows/VC.NET 2003 (warning level 4) + - Windows/VC 2005 (warning level 4) + - Windows/VC 2019 (warning level 4) + - Linux/gcc (-Wall) + - Mac OS/c++ (-Wall) + + @section usage USAGE SUMMARY + + -# Decide if you will be using utf8 or MBCS files, and working with the + data in utf8, wchar_t or ICU chars. + -# If you will only be using straight utf8 files and access the data via the + char interface, then you do not need any conversion library and could define + SI_NO_CONVERSION. Note that no conversion also means no validation of the data. + If no converter is specified then the default converter is SI_NO_CONVERSION + on Mac/Linux and SI_CONVERT_WIN32 on Windows. If you need widechar support on + Mac/Linux then use either SI_CONVERT_GENERIC or SI_CONVERT_ICU. These are also + supported on all platforms. + -# Define the appropriate symbol for the converter you wish to use and + include the SimpleIni.h header file. + -# Declare an instance of the appropriate class. Note that the following + definitions are just shortcuts for commonly used types. Other types + (PRUnichar, unsigned short, unsigned char) are also possible. + +
Interface Case-sensitive Load UTF-8 Load MBCS Typedef +
SI_NO_CONVERSION +
char No Yes No CSimpleIniA +
char Yes Yes No CSimpleIniCaseA +
SI_CONVERT_GENERIC +
char No Yes Yes #1 CSimpleIniA +
char Yes Yes Yes CSimpleIniCaseA +
wchar_t No Yes Yes CSimpleIniW +
wchar_t Yes Yes Yes CSimpleIniCaseW +
SI_CONVERT_WIN32 +
char No No #2 Yes CSimpleIniA +
char Yes Yes Yes CSimpleIniCaseA +
wchar_t No Yes Yes CSimpleIniW +
wchar_t Yes Yes Yes CSimpleIniCaseW +
SI_CONVERT_ICU +
char No Yes Yes CSimpleIniA +
char Yes Yes Yes CSimpleIniCaseA +
UChar No Yes Yes CSimpleIniW +
UChar Yes Yes Yes CSimpleIniCaseW +
+ #1 On Windows you are better to use CSimpleIniA with SI_CONVERT_WIN32.
+ #2 Only affects Windows. On Windows this uses MBCS functions and + so may fold case incorrectly leading to uncertain results. + -# Set all the options that you require, see all the Set*() options below. + The SetUnicode() option is very common and can be specified in the constructor. + -# Call LoadData() or LoadFile() to load and parse the INI configuration file + -# Access and modify the data of the file using the following functions + +
GetAllSections Return all section names +
GetAllKeys Return all key names within a section +
GetAllValues Return all values within a section & key +
GetSection Return all key names and values in a section +
GetSectionSize Return the number of keys in a section +
GetValue Return a value for a section & key +
SetValue Add or update a value for a section & key +
Delete Remove a section, or a key from a section +
SectionExists Does a section exist? +
KeyExists Does a key exist? +
+ -# Call Save() or SaveFile() to save the INI configuration data + + @section iostreams IO STREAMS + + SimpleIni supports reading from and writing to STL IO streams. Enable this + by defining SI_SUPPORT_IOSTREAMS before including the SimpleIni.h header + file. Ensure that if the streams are backed by a file (e.g. ifstream or + ofstream) then the flag ios_base::binary has been used when the file was + opened. + + @section multiline MULTI-LINE VALUES + + Values that span multiple lines are created using the following format. + +
+        key = <<
+
+    Note the following:
+    - The text used for ENDTAG can be anything and is used to find
+      where the multi-line text ends.
+    - The newline after ENDTAG in the start tag, and the newline
+      before ENDTAG in the end tag is not included in the data value.
+    - The ending tag must be on it's own line with no whitespace before
+      or after it.
+    - The multi-line value is modified at load so that each line in the value
+      is delimited by a single '\\n' character on all platforms. At save time
+      it will be converted into the newline format used by the current
+      platform.
+
+    @section comments COMMENTS
+
+    Comments are preserved in the file within the following restrictions:
+    - Every file may have a single "file comment". It must start with the
+      first character in the file, and will end with the first non-comment
+      line in the file.
+    - Every section may have a single "section comment". It will start
+      with the first comment line following the file comment, or the last
+      data entry. It ends at the beginning of the section.
+    - Every key may have a single "key comment". This comment will start
+      with the first comment line following the section start, or the file
+      comment if there is no section name.
+    - Comments are set at the time that the file, section or key is first
+      created. The only way to modify a comment on a section or a key is to
+      delete that entry and recreate it with the new comment. There is no
+      way to change the file comment.
+
+    @section save SAVE ORDER
+
+    The sections and keys are written out in the same order as they were
+    read in from the file. Sections and keys added to the data after the
+    file has been loaded will be added to the end of the file when it is
+    written. There is no way to specify the location of a section or key
+    other than in first-created, first-saved order.
+
+    @section notes NOTES
+
+    - The maximum supported file size is 1 GiB (SI_MAX_FILE_SIZE). Files larger
+      than this will be rejected with SI_FILE error to prevent excessive memory
+      allocation and potential denial of service attacks.
+    - To load UTF-8 data on Windows 95, you need to use Microsoft Layer for
+      Unicode, or SI_CONVERT_GENERIC, or SI_CONVERT_ICU.
+    - When using SI_CONVERT_GENERIC, ConvertUTF.c must be compiled and linked.
+    - When using SI_CONVERT_ICU, ICU header files must be on the include
+      path and icuuc.lib must be linked in.
+    - To load a UTF-8 file on Windows AND expose it with SI_CHAR == char,
+      you should use SI_CONVERT_GENERIC.
+    - The collation (sorting) order used for sections and keys returned from
+      iterators is NOT DEFINED. If collation order of the text is important
+      then it should be done yourself by either supplying a replacement
+      SI_STRLESS class, or by sorting the strings external to this library.
+    - Usage of the  header on Windows can be disabled by defining
+      SI_NO_MBCS. This is defined automatically on Windows CE platforms.
+    - Not thread-safe so manage your own locking
+
+    @section contrib CONTRIBUTIONS
+
+    Many thanks to the following contributors:
+    
+    - 2010/05/03: Tobias Gehrig: added GetDoubleValue()
+    - See list of many contributors in github
+
+    @section licence MIT LICENCE
+
+    The licence text below is the boilerplate "MIT Licence" used from:
+    http://www.opensource.org/licenses/mit-license.php
+
+    Copyright (c) 2006-2024, Brodie Thiesfield
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is furnished
+    to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+    FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+    COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+    IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+    CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+#ifndef INCLUDED_SimpleIni_h
+#define INCLUDED_SimpleIni_h
+
+#if defined(_MSC_VER) && (_MSC_VER >= 1020)
+# pragma once
+#endif
+
+// Disable these warnings in MSVC:
+//  4127 "conditional expression is constant" as the conversion classes trigger
+//  it with the statement if (sizeof(SI_CHAR) == sizeof(char)). This test will
+//  be optimized away in a release build.
+//  4503 'insert' : decorated name length exceeded, name was truncated
+//  4702 "unreachable code" as the MS STL header causes it in release mode.
+//  Again, the code causing the warning will be cleaned up by the compiler.
+//  4786 "identifier truncated to 256 characters" as this is thrown hundreds
+//  of times VC6 as soon as STL is used.
+#ifdef _MSC_VER
+# pragma warning (push)
+# pragma warning (disable: 4127 4503 4702 4786)
+#endif
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#ifdef SI_SUPPORT_IOSTREAMS
+# include 
+#endif // SI_SUPPORT_IOSTREAMS
+
+#ifdef _DEBUG
+# ifndef assert
+#  include 
+# endif
+# define SI_ASSERT(x)   assert(x)
+#else
+# define SI_ASSERT(x)
+#endif
+
+using SI_Error = int;
+
+constexpr int SI_OK = 0;        //!< No error
+constexpr int SI_UPDATED = 1;   //!< An existing value was updated
+constexpr int SI_INSERTED = 2;  //!< A new value was inserted
+
+// note: test for any error with (retval < 0)
+constexpr int SI_FAIL = -1;     //!< Generic failure
+constexpr int SI_NOMEM = -2;    //!< Out of memory error
+constexpr int SI_FILE = -3;     //!< File error (see errno for detail error)
+
+//! Maximum supported file size (1 GiB). Files larger than this will be rejected
+//! to prevent excessive memory allocation and potential denial of service.
+constexpr size_t SI_MAX_FILE_SIZE = 1024ULL * 1024ULL * 1024ULL;
+
+#define SI_UTF8_SIGNATURE     "\xEF\xBB\xBF"
+
+#ifdef _WIN32
+# define SI_NEWLINE_A   "\r\n"
+# define SI_NEWLINE_W   L"\r\n"
+#else // !_WIN32
+# define SI_NEWLINE_A   "\n"
+# define SI_NEWLINE_W   L"\n"
+#endif // _WIN32
+
+#if defined(SI_CONVERT_ICU)
+# include 
+#endif
+
+#if defined(_WIN32)
+# define SI_HAS_WIDE_FILE
+# define SI_WCHAR_T     wchar_t
+#elif defined(SI_CONVERT_ICU)
+# define SI_HAS_WIDE_FILE
+# define SI_WCHAR_T     UChar
+#endif
+
+
+// ---------------------------------------------------------------------------
+//                              MAIN TEMPLATE CLASS
+// ---------------------------------------------------------------------------
+
+/** Simple INI file reader.
+
+    This can be instantiated with the choice of unicode or native characterset,
+    and case sensitive or insensitive comparisons of section and key names.
+    The supported combinations are pre-defined with the following typedefs:
+
+    
+        
Interface Case-sensitive Typedef +
char No CSimpleIniA +
char Yes CSimpleIniCaseA +
wchar_t No CSimpleIniW +
wchar_t Yes CSimpleIniCaseW +
+ + Note that using other types for the SI_CHAR is supported. For instance, + unsigned char, unsigned short, etc. Note that where the alternative type + is a different size to char/wchar_t you may need to supply new helper + classes for SI_STRLESS and SI_CONVERTER. + */ +template +class CSimpleIniTempl +{ +public: + typedef SI_CHAR SI_CHAR_T; + + /** key entry */ + struct Entry { + const SI_CHAR * pItem; + const SI_CHAR * pComment; + int nOrder; + + Entry(const SI_CHAR * a_pszItem = NULL, int a_nOrder = 0) + : pItem(a_pszItem) + , pComment(NULL) + , nOrder(a_nOrder) + { } + Entry(const SI_CHAR * a_pszItem, const SI_CHAR * a_pszComment, int a_nOrder) + : pItem(a_pszItem) + , pComment(a_pszComment) + , nOrder(a_nOrder) + { } + Entry(const Entry & rhs) { operator=(rhs); } + Entry & operator=(const Entry & rhs) { + pItem = rhs.pItem; + pComment = rhs.pComment; + nOrder = rhs.nOrder; + return *this; + } + +#if defined(_MSC_VER) && _MSC_VER <= 1200 + /** STL of VC6 doesn't allow me to specify my own comparator for list::sort() */ + bool operator<(const Entry & rhs) const { return LoadOrder()(*this, rhs); } + bool operator>(const Entry & rhs) const { return LoadOrder()(rhs, *this); } +#endif + + /** Strict less ordering by name of key only */ + struct KeyOrder { + bool operator()(const Entry & lhs, const Entry & rhs) const { + const static SI_STRLESS isLess = SI_STRLESS(); + return isLess(lhs.pItem, rhs.pItem); + } + }; + + /** Strict less ordering by order, and then name of key */ + struct LoadOrder { + bool operator()(const Entry & lhs, const Entry & rhs) const { + if (lhs.nOrder != rhs.nOrder) { + return lhs.nOrder < rhs.nOrder; + } + return KeyOrder()(lhs, rhs); + } + }; + }; + + /** map keys to values */ + typedef std::multimap TKeyVal; + + /** map sections to key/value map */ + typedef std::map TSection; + + /** set of dependent string pointers. Note that these pointers are + dependent on memory owned by CSimpleIni. + */ + typedef std::list TNamesDepend; + + /** interface definition for the OutputWriter object to pass to Save() + in order to output the INI file data. + */ + class OutputWriter { + public: + OutputWriter() { } + virtual ~OutputWriter() { } + virtual void Write(const char * a_pBuf) = 0; + private: + OutputWriter(const OutputWriter &); // disable + OutputWriter & operator=(const OutputWriter &); // disable + }; + + /** OutputWriter class to write the INI data to a file */ + class FileWriter : public OutputWriter { + FILE * m_file; + public: + FileWriter(FILE * a_file) : m_file(a_file) { } + void Write(const char * a_pBuf) { + fputs(a_pBuf, m_file); + } + private: + FileWriter(const FileWriter &); // disable + FileWriter & operator=(const FileWriter &); // disable + }; + + /** OutputWriter class to write the INI data to a string */ + class StringWriter : public OutputWriter { + std::string & m_string; + public: + StringWriter(std::string & a_string) : m_string(a_string) { } + void Write(const char * a_pBuf) { + m_string.append(a_pBuf); + } + private: + StringWriter(const StringWriter &); // disable + StringWriter & operator=(const StringWriter &); // disable + }; + +#ifdef SI_SUPPORT_IOSTREAMS + /** OutputWriter class to write the INI data to an ostream */ + class StreamWriter : public OutputWriter { + std::ostream & m_ostream; + public: + StreamWriter(std::ostream & a_ostream) : m_ostream(a_ostream) { } + void Write(const char * a_pBuf) { + m_ostream << a_pBuf; + } + private: + StreamWriter(const StreamWriter &); // disable + StreamWriter & operator=(const StreamWriter &); // disable + }; +#endif // SI_SUPPORT_IOSTREAMS + + /** Characterset conversion utility class to convert strings to the + same format as is used for the storage. + */ + class Converter : private SI_CONVERTER { + public: + Converter(bool a_bStoreIsUtf8) : SI_CONVERTER(a_bStoreIsUtf8) { + m_scratch.resize(1024); + } + Converter(const Converter & rhs) { operator=(rhs); } + Converter & operator=(const Converter & rhs) { + m_scratch = rhs.m_scratch; + return *this; + } + bool ConvertToStore(const SI_CHAR * a_pszString) { + size_t uLen = SI_CONVERTER::SizeToStore(a_pszString); + if (uLen == (size_t)(-1)) { + return false; + } + while (uLen > m_scratch.size()) { + m_scratch.resize(m_scratch.size() * 2); + } + return SI_CONVERTER::ConvertToStore( + a_pszString, + const_cast(m_scratch.data()), + m_scratch.size()); + } + const char * Data() { return m_scratch.data(); } + private: + std::string m_scratch; + }; + +public: + /*-----------------------------------------------------------------------*/ + + /** Default constructor. + + @param a_bIsUtf8 See the method SetUnicode() for details. + @param a_bMultiKey See the method SetMultiKey() for details. + @param a_bMultiLine See the method SetMultiLine() for details. + */ + CSimpleIniTempl( + bool a_bIsUtf8 = false, + bool a_bMultiKey = false, + bool a_bMultiLine = false + ); + + /** Destructor */ + ~CSimpleIniTempl(); + + /** Deallocate all memory stored by this object */ + void Reset(); + + /** Has any data been loaded */ + bool IsEmpty() const { return m_data.empty(); } + + /*-----------------------------------------------------------------------*/ + /** @{ @name Settings */ + + /** Set the storage format of the INI data. This affects both the loading + and saving of the INI data using all of the Load/Save API functions. + This value cannot be changed after any INI data has been loaded. + + If the file is not set to Unicode (UTF-8), then the data encoding is + assumed to be the OS native encoding. This encoding is the system + locale on Linux/Unix and the legacy MBCS encoding on Windows NT/2K/XP. + If the storage format is set to Unicode then the file will be loaded + as UTF-8 encoded data regardless of the native file encoding. If + SI_CHAR == char then all of the char* parameters take and return UTF-8 + encoded data regardless of the system locale. + + \param a_bIsUtf8 Assume UTF-8 encoding for the source? + */ + void SetUnicode(bool a_bIsUtf8 = true) { + if (!m_pData) m_bStoreIsUtf8 = a_bIsUtf8; + } + + /** Get the storage format of the INI data. */ + bool IsUnicode() const { return m_bStoreIsUtf8; } + + /** Should multiple identical keys be permitted in the file. If set to false + then the last value encountered will be used as the value of the key. + If set to true, then all values will be available to be queried. For + example, with the following input: + +
+        [section]
+        test=value1
+        test=value2
+        
+ + Then with SetMultiKey(true), both of the values "value1" and "value2" + will be returned for the key test. If SetMultiKey(false) is used, then + the value for "test" will only be "value2". This value may be changed + at any time. + + \param a_bAllowMultiKey Allow multi-keys in the source? + */ + void SetMultiKey(bool a_bAllowMultiKey = true) { + m_bAllowMultiKey = a_bAllowMultiKey; + } + + /** Get the storage format of the INI data. */ + bool IsMultiKey() const { return m_bAllowMultiKey; } + + /** Should data values be permitted to span multiple lines in the file. If + set to false then the multi-line construct << + SI_CHAR FORMAT + char same format as when loaded (MBCS or UTF-8) + wchar_t UTF-8 + other UTF-8 + + + Note that comments from the original data is preserved as per the + documentation on comments. The order of the sections and values + from the original file will be preserved. + + Any data prepended or appended to the output device must use the the + same format (MBCS or UTF-8). You may use the GetConverter() method to + convert text to the correct format regardless of the output format + being used by SimpleIni. + + To add a BOM to UTF-8 data, write it out manually at the very beginning + like is done in SaveFile when a_bUseBOM is true. + + @param a_oOutput Output writer to write the data to. + + @param a_bAddSignature Prepend the UTF-8 BOM if the output data is in + UTF-8 format. If it is not UTF-8 then this value is + ignored. Do not set this to true if anything has + already been written to the OutputWriter. + + @return SI_Error See error definitions + */ + SI_Error Save( + OutputWriter & a_oOutput, + bool a_bAddSignature = false + ) const; + +#ifdef SI_SUPPORT_IOSTREAMS + /** Save the INI data to an ostream. See Save() for details. + + @param a_ostream String to have the INI data appended to. + + @param a_bAddSignature Prepend the UTF-8 BOM if the output data is in + UTF-8 format. If it is not UTF-8 then this value is + ignored. Do not set this to true if anything has + already been written to the stream. + + @return SI_Error See error definitions + */ + SI_Error Save( + std::ostream & a_ostream, + bool a_bAddSignature = false + ) const + { + StreamWriter writer(a_ostream); + return Save(writer, a_bAddSignature); + } +#endif // SI_SUPPORT_IOSTREAMS + + /** Append the INI data to a string. See Save() for details. + + @param a_sBuffer String to have the INI data appended to. + + @param a_bAddSignature Prepend the UTF-8 BOM if the output data is in + UTF-8 format. If it is not UTF-8 then this value is + ignored. Do not set this to true if anything has + already been written to the string. + + @return SI_Error See error definitions + */ + SI_Error Save( + std::string & a_sBuffer, + bool a_bAddSignature = false + ) const + { + StringWriter writer(a_sBuffer); + return Save(writer, a_bAddSignature); + } + + /*-----------------------------------------------------------------------*/ + /** @} + @{ @name Accessing INI Data */ + + /** Retrieve all section names. The list is returned as an STL vector of + names and can be iterated or searched as necessary. Note that the + sort order of the returned strings is NOT DEFINED. You can sort + the names into the load order if desired. Search this file for ".sort" + for an example. + + NOTE! This structure contains only pointers to strings. The actual + string data is stored in memory owned by CSimpleIni. Ensure that the + CSimpleIni object is not destroyed or Reset() while these pointers + are in use! + + @param a_names Vector that will receive all of the section + names. See note above! + */ + void GetAllSections( + TNamesDepend & a_names + ) const; + + /** Retrieve all unique key names in a section. The sort order of the + returned strings is NOT DEFINED. You can sort the names into the load + order if desired. Search this file for ".sort" for an example. Only + unique key names are returned. + + NOTE! This structure contains only pointers to strings. The actual + string data is stored in memory owned by CSimpleIni. Ensure that the + CSimpleIni object is not destroyed or Reset() while these strings + are in use! + + @param a_pSection Section to request data for + @param a_names List that will receive all of the key + names. See note above! + + @return true Section was found. + @return false Matching section was not found. + */ + bool GetAllKeys( + const SI_CHAR * a_pSection, + TNamesDepend & a_names + ) const; + + /** Retrieve all values for a specific key. This method can be used when + multiple keys are both enabled and disabled. Note that the sort order + of the returned strings is NOT DEFINED. You can sort the names into + the load order if desired. Search this file for ".sort" for an example. + + NOTE! The returned values are pointers to string data stored in memory + owned by CSimpleIni. Ensure that the CSimpleIni object is not destroyed + or Reset while you are using this pointer! + + @param a_pSection Section to search + @param a_pKey Key to search for + @param a_values List to return if the key is not found + + @return true Key was found. + @return false Matching section/key was not found. + */ + bool GetAllValues( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + TNamesDepend & a_values + ) const; + + /** Query the number of keys in a specific section. Note that if multiple + keys are enabled, then this value may be different to the number of + keys returned by GetAllKeys. + + @param a_pSection Section to request data for + + @return -1 Section does not exist in the file + @return >=0 Number of keys in the section + */ + int GetSectionSize( + const SI_CHAR * a_pSection + ) const; + + /** Retrieve all key and value pairs for a section. The data is returned + as a pointer to an STL map and can be iterated or searched as + desired. Note that multiple entries for the same key may exist when + multiple keys have been enabled. + + NOTE! This structure contains only pointers to strings. The actual + string data is stored in memory owned by CSimpleIni. Ensure that the + CSimpleIni object is not destroyed or Reset() while these strings + are in use! + + @param a_pSection Name of the section to return + @return Section data + */ + const TKeyVal * GetSection( + const SI_CHAR * a_pSection + ) const; + + /** Test if a section exists. Convenience function */ + inline bool SectionExists( + const SI_CHAR * a_pSection + ) const { + return GetSection(a_pSection) != NULL; + } + + /** Test if the key exists in a section. Convenience function. */ + inline bool KeyExists( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey + ) const { + return GetValue(a_pSection, a_pKey) != NULL; + } + + /** Retrieve the value for a specific key. If multiple keys are enabled + (see SetMultiKey) then only the first value associated with that key + will be returned, see GetAllValues for getting all values with multikey. + + NOTE! The returned value is a pointer to string data stored in memory + owned by CSimpleIni. Ensure that the CSimpleIni object is not destroyed + or Reset while you are using this pointer! + + @param a_pSection Section to search + @param a_pKey Key to search for + @param a_pDefault Value to return if the key is not found + @param a_pHasMultiple Optionally receive notification of if there are + multiple entries for this key. + + @return a_pDefault Key was not found in the section + @return other Value of the key + */ + const SI_CHAR * GetValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + const SI_CHAR * a_pDefault = NULL, + bool * a_pHasMultiple = NULL + ) const; + + /** Retrieve a numeric value for a specific key. If multiple keys are enabled + (see SetMultiKey) then only the first value associated with that key + will be returned, see GetAllValues for getting all values with multikey. + + @param a_pSection Section to search + @param a_pKey Key to search for + @param a_nDefault Value to return if the key is not found + @param a_pHasMultiple Optionally receive notification of if there are + multiple entries for this key. + + @return a_nDefault Key was not found in the section + @return other Value of the key + */ + long GetLongValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + long a_nDefault = 0, + bool * a_pHasMultiple = NULL + ) const; + + /** Retrieve a numeric value for a specific key. If multiple keys are enabled + (see SetMultiKey) then only the first value associated with that key + will be returned, see GetAllValues for getting all values with multikey. + + @param a_pSection Section to search + @param a_pKey Key to search for + @param a_nDefault Value to return if the key is not found + @param a_pHasMultiple Optionally receive notification of if there are + multiple entries for this key. + + @return a_nDefault Key was not found in the section + @return other Value of the key + */ + double GetDoubleValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + double a_nDefault = 0, + bool * a_pHasMultiple = NULL + ) const; + + /** Retrieve a boolean value for a specific key. If multiple keys are enabled + (see SetMultiKey) then only the first value associated with that key + will be returned, see GetAllValues for getting all values with multikey. + + Strings starting with "t", "y", "on" or "1" are returned as logically true. + Strings starting with "f", "n", "of" or "0" are returned as logically false. + For all other values the default is returned. Character comparisons are + case-insensitive. + + @param a_pSection Section to search + @param a_pKey Key to search for + @param a_bDefault Value to return if the key is not found + @param a_pHasMultiple Optionally receive notification of if there are + multiple entries for this key. + + @return a_nDefault Key was not found in the section + @return other Value of the key + */ + bool GetBoolValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + bool a_bDefault = false, + bool * a_pHasMultiple = NULL + ) const; + + /** Add or update a section or value. This will always insert + when multiple keys are enabled. + + @param a_pSection Section to add or update + @param a_pKey Key to add or update. Set to NULL to + create an empty section. + @param a_pValue Value to set. Set to NULL to create an + empty section. + @param a_pComment Comment to be associated with the section or the + key. If a_pKey is NULL then it will be associated + with the section, otherwise the key. Note that a + comment may be set ONLY when the section or key is + first created (i.e. when this function returns the + value SI_INSERTED). If you wish to create a section + with a comment then you need to create the section + separately to the key. The comment string must be + in full comment form already (have a comment + character starting every line). + @param a_bForceReplace Should all existing values in a multi-key INI + file be replaced with this entry. This option has + no effect if not using multi-key files. The + difference between Delete/SetValue and SetValue + with a_bForceReplace = true, is that the load + order and comment will be preserved this way. + + @return SI_Error See error definitions + @return SI_UPDATED Value was updated + @return SI_INSERTED Value was inserted + */ + SI_Error SetValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + const SI_CHAR * a_pValue, + const SI_CHAR * a_pComment = NULL, + bool a_bForceReplace = false + ) + { + return AddEntry(a_pSection, a_pKey, a_pValue, a_pComment, a_bForceReplace, true); + } + + /** Add or update a numeric value. This will always insert + when multiple keys are enabled. + + @param a_pSection Section to add or update + @param a_pKey Key to add or update. + @param a_nValue Value to set. + @param a_pComment Comment to be associated with the key. See the + notes on SetValue() for comments. + @param a_bUseHex By default the value will be written to the file + in decimal format. Set this to true to write it + as hexadecimal. + @param a_bForceReplace Should all existing values in a multi-key INI + file be replaced with this entry. This option has + no effect if not using multi-key files. The + difference between Delete/SetLongValue and + SetLongValue with a_bForceReplace = true, is that + the load order and comment will be preserved this + way. + + @return SI_Error See error definitions + @return SI_UPDATED Value was updated + @return SI_INSERTED Value was inserted + */ + SI_Error SetLongValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + long a_nValue, + const SI_CHAR * a_pComment = NULL, + bool a_bUseHex = false, + bool a_bForceReplace = false + ); + + /** Add or update a double value. This will always insert + when multiple keys are enabled. + + @param a_pSection Section to add or update + @param a_pKey Key to add or update. + @param a_nValue Value to set. + @param a_pComment Comment to be associated with the key. See the + notes on SetValue() for comments. + @param a_bForceReplace Should all existing values in a multi-key INI + file be replaced with this entry. This option has + no effect if not using multi-key files. The + difference between Delete/SetDoubleValue and + SetDoubleValue with a_bForceReplace = true, is that + the load order and comment will be preserved this + way. + + @return SI_Error See error definitions + @return SI_UPDATED Value was updated + @return SI_INSERTED Value was inserted + */ + SI_Error SetDoubleValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + double a_nValue, + const SI_CHAR * a_pComment = NULL, + bool a_bForceReplace = false + ); + + /** Add or update a boolean value. This will always insert + when multiple keys are enabled. + + @param a_pSection Section to add or update + @param a_pKey Key to add or update. + @param a_bValue Value to set. + @param a_pComment Comment to be associated with the key. See the + notes on SetValue() for comments. + @param a_bForceReplace Should all existing values in a multi-key INI + file be replaced with this entry. This option has + no effect if not using multi-key files. The + difference between Delete/SetBoolValue and + SetBoolValue with a_bForceReplace = true, is that + the load order and comment will be preserved this + way. + + @return SI_Error See error definitions + @return SI_UPDATED Value was updated + @return SI_INSERTED Value was inserted + */ + SI_Error SetBoolValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + bool a_bValue, + const SI_CHAR * a_pComment = NULL, + bool a_bForceReplace = false + ); + + /** Delete an entire section, or a key from a section. Note that the + data returned by GetSection is invalid and must not be used after + anything has been deleted from that section using this method. + Note when multiple keys is enabled, this will delete all keys with + that name; to selectively delete individual key/values, use + DeleteValue. + + @param a_pSection Section to delete key from, or if + a_pKey is NULL, the section to remove. + @param a_pKey Key to remove from the section. Set to + NULL to remove the entire section. + @param a_bRemoveEmpty If the section is empty after this key has + been deleted, should the empty section be + removed? + + @return true Key or section was deleted. + @return false Key or section was not found. + */ + bool Delete( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + bool a_bRemoveEmpty = false + ); + + /** Delete an entire section, or a key from a section. If value is + provided, only remove keys with the value. Note that the data + returned by GetSection is invalid and must not be used after + anything has been deleted from that section using this method. + Note when multiple keys is enabled, all keys with the value will + be deleted. + + @param a_pSection Section to delete key from, or if + a_pKey is NULL, the section to remove. + @param a_pKey Key to remove from the section. Set to + NULL to remove the entire section. + @param a_pValue Value of key to remove from the section. + Set to NULL to remove all keys. + @param a_bRemoveEmpty If the section is empty after this key has + been deleted, should the empty section be + removed? + + @return true Key/value or section was deleted. + @return false Key/value or section was not found. + */ + bool DeleteValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + const SI_CHAR * a_pValue, + bool a_bRemoveEmpty = false + ); + + /*-----------------------------------------------------------------------*/ + /** @} + @{ @name Converter */ + + /** Return a conversion object to convert text to the same encoding + as is used by the Save(), SaveFile() and SaveString() functions. + Use this to prepare the strings that you wish to append or prepend + to the output INI data. + */ + Converter GetConverter() const { + return Converter(m_bStoreIsUtf8); + } + + /*-----------------------------------------------------------------------*/ + /** @} */ + +private: + // copying is not permitted + CSimpleIniTempl(const CSimpleIniTempl &); // disabled + CSimpleIniTempl & operator=(const CSimpleIniTempl &); // disabled + + /** Parse the data looking for a file comment and store it if found. + */ + SI_Error FindFileComment( + SI_CHAR *& a_pData, + bool a_bCopyStrings + ); + + /** Parse the data looking for the next valid entry. The memory pointed to + by a_pData is modified by inserting NULL characters. The pointer is + updated to the current location in the block of text. + */ + bool FindEntry( + SI_CHAR *& a_pData, + const SI_CHAR *& a_pSection, + const SI_CHAR *& a_pKey, + const SI_CHAR *& a_pVal, + const SI_CHAR *& a_pComment + ) const; + + /** Add the section/key/value to our data. + + @param a_pSection Section name. Sections will be created if they + don't already exist. + @param a_pKey Key name. May be NULL to create an empty section. + Existing entries will be updated. New entries will + be created. + @param a_pValue Value for the key. + @param a_pComment Comment to be associated with the section or the + key. If a_pKey is NULL then it will be associated + with the section, otherwise the key. This must be + a string in full comment form already (have a + comment character starting every line). + @param a_bForceReplace Should all existing values in a multi-key INI + file be replaced with this entry. This option has + no effect if not using multi-key files. The + difference between Delete/AddEntry and AddEntry + with a_bForceReplace = true, is that the load + order and comment will be preserved this way. + @param a_bCopyStrings Should copies of the strings be made or not. + If false then the pointers will be used as is. + */ + SI_Error AddEntry( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + const SI_CHAR * a_pValue, + const SI_CHAR * a_pComment, + bool a_bForceReplace, + bool a_bCopyStrings + ); + + /** Is the supplied character a whitespace character? */ + inline bool IsSpace(SI_CHAR ch) const { + return (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'); + } + + /** Does the supplied character start a comment line? */ + inline bool IsComment(SI_CHAR ch) const { + return (ch == ';' || ch == '#'); + } + + + /** Skip over a newline character (or characters) for either DOS or UNIX */ + inline void SkipNewLine(SI_CHAR *& a_pData) const { + a_pData += (*a_pData == '\r' && *(a_pData+1) == '\n') ? 2 : 1; + } + + /** Make a copy of the supplied string, replacing the original pointer */ + SI_Error CopyString(const SI_CHAR *& a_pString); + + /** Delete a string from the copied strings buffer if necessary */ + void DeleteString(const SI_CHAR * a_pString); + + /** Internal use of our string comparison function */ + bool IsLess(const SI_CHAR * a_pLeft, const SI_CHAR * a_pRight) const { + const static SI_STRLESS isLess = SI_STRLESS(); + return isLess(a_pLeft, a_pRight); + } + + bool IsMultiLineTag(const SI_CHAR * a_pData) const; + bool IsMultiLineData(const SI_CHAR * a_pData) const; + bool IsSingleLineQuotedValue(const SI_CHAR* a_pData) const; + bool LoadMultiLineText( + SI_CHAR *& a_pData, + const SI_CHAR *& a_pVal, + const SI_CHAR * a_pTagName, + bool a_bAllowBlankLinesInComment = false + ) const; + bool IsNewLineChar(SI_CHAR a_c) const; + + bool OutputMultiLineText( + OutputWriter & a_oOutput, + Converter & a_oConverter, + const SI_CHAR * a_pText + ) const; + +private: + /** Copy of the INI file data in our character format. This will be + modified when parsed to have NULL characters added after all + interesting string entries. All of the string pointers to sections, + keys and values point into this block of memory. + */ + SI_CHAR * m_pData; + + /** Length of the data that we have stored. Used when deleting strings + to determine if the string is stored here or in the allocated string + buffer. + */ + size_t m_uDataLen; + + /** File comment for this data, if one exists. */ + const SI_CHAR * m_pFileComment; + + /** constant empty string */ + const SI_CHAR m_cEmptyString; + + /** Parsed INI data. Section -> (Key -> Value). */ + TSection m_data; + + /** This vector stores allocated memory for copies of strings that have + been supplied after the file load. It will be empty unless SetValue() + has been called. + */ + TNamesDepend m_strings; + + /** Is the format of our datafile UTF-8 or MBCS? */ + bool m_bStoreIsUtf8; + + /** Are multiple values permitted for the same key? */ + bool m_bAllowMultiKey; + + /** Are data values permitted to span multiple lines? */ + bool m_bAllowMultiLine; + + /** Should spaces be written out surrounding the equals sign? */ + bool m_bSpaces; + + /** Should quoted data in values be recognized and parsed? */ + bool m_bParseQuotes; + + /** Do keys always need to have an equals sign when reading/writing? */ + bool m_bAllowKeyOnly; + + /** Next order value, used to ensure sections and keys are output in the + same order that they are loaded/added. + */ + int m_nOrder; +}; + +// --------------------------------------------------------------------------- +// IMPLEMENTATION +// --------------------------------------------------------------------------- + +template +CSimpleIniTempl::CSimpleIniTempl( + bool a_bIsUtf8, + bool a_bAllowMultiKey, + bool a_bAllowMultiLine + ) + : m_pData(0) + , m_uDataLen(0) + , m_pFileComment(NULL) + , m_cEmptyString(0) + , m_bStoreIsUtf8(a_bIsUtf8) + , m_bAllowMultiKey(a_bAllowMultiKey) + , m_bAllowMultiLine(a_bAllowMultiLine) + , m_bSpaces(true) + , m_bParseQuotes(false) + , m_bAllowKeyOnly(false) + , m_nOrder(0) +{ } + +template +CSimpleIniTempl::~CSimpleIniTempl() +{ + Reset(); +} + +template +void +CSimpleIniTempl::Reset() +{ + // remove all data + delete[] m_pData; + m_pData = NULL; + m_uDataLen = 0; + m_pFileComment = NULL; + if (!m_data.empty()) { + m_data.erase(m_data.begin(), m_data.end()); + } + + // remove all strings + if (!m_strings.empty()) { + typename TNamesDepend::iterator i = m_strings.begin(); + for (; i != m_strings.end(); ++i) { + delete[] const_cast(i->pItem); + } + m_strings.erase(m_strings.begin(), m_strings.end()); + } +} + +template +SI_Error +CSimpleIniTempl::LoadFile( + const char * a_pszFile + ) +{ + FILE * fp = NULL; +#if __STDC_WANT_SECURE_LIB__ && !_WIN32_WCE + fopen_s(&fp, a_pszFile, "rb"); +#else // !__STDC_WANT_SECURE_LIB__ + fp = fopen(a_pszFile, "rb"); +#endif // __STDC_WANT_SECURE_LIB__ + if (!fp) { + return SI_FILE; + } + SI_Error rc = LoadFile(fp); + fclose(fp); + return rc; +} + +#ifdef SI_HAS_WIDE_FILE +template +SI_Error +CSimpleIniTempl::LoadFile( + const SI_WCHAR_T * a_pwszFile + ) +{ +#ifdef _WIN32 + FILE * fp = NULL; +#if __STDC_WANT_SECURE_LIB__ && !_WIN32_WCE + _wfopen_s(&fp, a_pwszFile, L"rb"); +#else // !__STDC_WANT_SECURE_LIB__ + fp = _wfopen(a_pwszFile, L"rb"); +#endif // __STDC_WANT_SECURE_LIB__ + if (!fp) return SI_FILE; + SI_Error rc = LoadFile(fp); + fclose(fp); + return rc; +#else // !_WIN32 (therefore SI_CONVERT_ICU) + char szFile[256]; + u_austrncpy(szFile, a_pwszFile, sizeof(szFile)); + return LoadFile(szFile); +#endif // _WIN32 +} +#endif // SI_HAS_WIDE_FILE + +template +SI_Error +CSimpleIniTempl::LoadFile( + FILE * a_fpFile + ) +{ + // load the raw file data + int retval = fseek(a_fpFile, 0, SEEK_END); + if (retval != 0) { + return SI_FILE; + } + long lSize = ftell(a_fpFile); + if (lSize < 0) { + return SI_FILE; + } + if (lSize == 0) { + return SI_OK; + } + + // check file size is within supported limits (SI_MAX_FILE_SIZE) + if (static_cast(lSize) > SI_MAX_FILE_SIZE) { + return SI_FILE; + } + + // allocate and ensure NULL terminated + char * pData = new(std::nothrow) char[static_cast(lSize) + 1]; + if (!pData) { + return SI_NOMEM; + } + pData[lSize] = 0; + + // load data into buffer + fseek(a_fpFile, 0, SEEK_SET); + size_t uRead = fread(pData, sizeof(char), lSize, a_fpFile); + if (uRead != (size_t) lSize) { + delete[] pData; + return SI_FILE; + } + + // convert the raw data to unicode + SI_Error rc = LoadData(pData, uRead); + delete[] pData; + return rc; +} + +template +SI_Error +CSimpleIniTempl::LoadData( + const char * a_pData, + size_t a_uDataLen + ) +{ + if (!a_pData) { + return SI_OK; + } + + // if the UTF-8 BOM exists, consume it and set mode to unicode, if we have + // already loaded data and try to change mode half-way through then this will + // be ignored and we will assert in debug versions + if (a_uDataLen >= 3 && memcmp(a_pData, SI_UTF8_SIGNATURE, 3) == 0) { + a_pData += 3; + a_uDataLen -= 3; + SI_ASSERT(m_bStoreIsUtf8 || !m_pData); // we don't expect mixed mode data + SetUnicode(); + } + + if (a_uDataLen == 0) { + return SI_OK; + } + + // determine the length of the converted data + SI_CONVERTER converter(m_bStoreIsUtf8); + size_t uLen = converter.SizeFromStore(a_pData, a_uDataLen); + if (uLen == (size_t)(-1)) { + return SI_FAIL; + } + + // check converted data size is within supported limits (SI_MAX_FILE_SIZE) + if (uLen >= (SI_MAX_FILE_SIZE / sizeof(SI_CHAR))) { + return SI_FILE; + } + + // allocate memory for the data, ensure that there is a NULL + // terminator wherever the converted data ends + SI_CHAR * pData = new(std::nothrow) SI_CHAR[uLen + 1]; + if (!pData) { + return SI_NOMEM; + } + memset(pData, 0, sizeof(SI_CHAR) * (uLen + 1)); + + // convert the data + if (!converter.ConvertFromStore(a_pData, a_uDataLen, pData, uLen)) { + delete[] pData; + return SI_FAIL; + } + + // parse it + const static SI_CHAR empty = 0; + SI_CHAR * pWork = pData; + const SI_CHAR * pSection = ∅ + const SI_CHAR * pItem = NULL; + const SI_CHAR * pVal = NULL; + const SI_CHAR * pComment = NULL; + + // We copy the strings if we are loading data into this class when we + // already have stored some. + bool bCopyStrings = (m_pData != NULL); + + // find a file comment if it exists, this is a comment that starts at the + // beginning of the file and continues until the first blank line. + SI_Error rc = FindFileComment(pWork, bCopyStrings); + if (rc < 0) return rc; + + // add every entry in the file to the data table + while (FindEntry(pWork, pSection, pItem, pVal, pComment)) { + rc = AddEntry(pSection, pItem, pVal, pComment, false, bCopyStrings); + if (rc < 0) return rc; + } + + // store these strings if we didn't copy them + if (bCopyStrings) { + delete[] pData; + } + else { + m_pData = pData; + m_uDataLen = uLen+1; + } + + return SI_OK; +} + +#ifdef SI_SUPPORT_IOSTREAMS +template +SI_Error +CSimpleIniTempl::LoadData( + std::istream & a_istream + ) +{ + std::string strData; + char szBuf[512]; + do { + a_istream.get(szBuf, sizeof(szBuf), '\0'); + strData.append(szBuf); + } + while (a_istream.good()); + return LoadData(strData); +} +#endif // SI_SUPPORT_IOSTREAMS + +template +SI_Error +CSimpleIniTempl::FindFileComment( + SI_CHAR *& a_pData, + bool a_bCopyStrings + ) +{ + // there can only be a single file comment + if (m_pFileComment) { + return SI_OK; + } + + // Load the file comment as multi-line text, this will modify all of + // the newline characters to be single \n chars + if (!LoadMultiLineText(a_pData, m_pFileComment, NULL, false)) { + return SI_OK; + } + + // copy the string if necessary + if (a_bCopyStrings) { + SI_Error rc = CopyString(m_pFileComment); + if (rc < 0) return rc; + } + + return SI_OK; +} + +template +bool +CSimpleIniTempl::FindEntry( + SI_CHAR *& a_pData, + const SI_CHAR *& a_pSection, + const SI_CHAR *& a_pKey, + const SI_CHAR *& a_pVal, + const SI_CHAR *& a_pComment + ) const +{ + a_pComment = NULL; + + bool bHaveValue = false; + SI_CHAR * pTrail = NULL; + while (*a_pData) { + // skip spaces and empty lines + while (*a_pData && IsSpace(*a_pData)) { + ++a_pData; + } + if (!*a_pData) { + break; + } + + // skip processing of comment lines but keep a pointer to + // the start of the comment. + if (IsComment(*a_pData)) { + LoadMultiLineText(a_pData, a_pComment, NULL, true); + continue; + } + + // process section names + if (*a_pData == '[') { + // skip leading spaces + ++a_pData; + while (*a_pData && IsSpace(*a_pData)) { + ++a_pData; + } + + // find the end of the section name (it may contain spaces) + // and convert it to lowercase as necessary + a_pSection = a_pData; + while (*a_pData && *a_pData != ']' && !IsNewLineChar(*a_pData)) { + ++a_pData; + } + + // if it's an invalid line, just skip it + if (*a_pData != ']') { + continue; + } + + // remove trailing spaces from the section + pTrail = a_pData - 1; + while (pTrail >= a_pSection && IsSpace(*pTrail)) { + --pTrail; + } + ++pTrail; + *pTrail = 0; + + // skip to the end of the line + ++a_pData; // safe as checked that it == ']' above + while (*a_pData && !IsNewLineChar(*a_pData)) { + ++a_pData; + } + + a_pKey = NULL; + a_pVal = NULL; + return true; + } + + // find the end of the key name (it may contain spaces) + a_pKey = a_pData; + while (*a_pData && *a_pData != '=' && !IsNewLineChar(*a_pData)) { + ++a_pData; + } + // *a_pData is null, equals, or newline + + // if no value and we don't allow no value, then invalid + bHaveValue = (*a_pData == '='); + if (!bHaveValue && !m_bAllowKeyOnly) { + continue; + } + + // empty keys are invalid + if (bHaveValue && a_pKey == a_pData) { + while (*a_pData && !IsNewLineChar(*a_pData)) { + ++a_pData; + } + continue; + } + + // remove trailing spaces from the key + pTrail = a_pData - 1; + while (pTrail >= a_pKey && IsSpace(*pTrail)) { + --pTrail; + } + ++pTrail; + + if (bHaveValue) { + // process the value + *pTrail = 0; + + // skip leading whitespace on the value + ++a_pData; // safe as checked that it == '=' above + while (*a_pData && !IsNewLineChar(*a_pData) && IsSpace(*a_pData)) { + ++a_pData; + } + + // find the end of the value which is the end of this line + a_pVal = a_pData; + while (*a_pData && !IsNewLineChar(*a_pData)) { + ++a_pData; + } + + // remove trailing spaces from the value + pTrail = a_pData - 1; + if (*a_pData) { // prepare for the next round + SkipNewLine(a_pData); + } + while (pTrail >= a_pVal && IsSpace(*pTrail)) { + --pTrail; + } + ++pTrail; + *pTrail = 0; + + // check for multi-line entries + if (m_bAllowMultiLine && IsMultiLineTag(a_pVal)) { + // skip the "<<<" to get the tag that will end the multiline + const SI_CHAR* pTagName = a_pVal + 3; + return LoadMultiLineText(a_pData, a_pVal, pTagName); + } + + // check for quoted values, we are not supporting escapes in quoted values (yet) + if (m_bParseQuotes) { + --pTrail; + if (pTrail > a_pVal && *a_pVal == '"' && *pTrail == '"') { + ++a_pVal; + *pTrail = 0; + } + } + } + else { + // no value to process, just prepare for the next + if (*a_pData) { + SkipNewLine(a_pData); + } + *pTrail = 0; + } + + // return the standard entry + return true; + } + + return false; +} + +template +bool +CSimpleIniTempl::IsMultiLineTag( + const SI_CHAR * a_pVal + ) const +{ + // check for the "<<<" prefix for a multi-line entry + if (*a_pVal++ != '<') return false; + if (*a_pVal++ != '<') return false; + if (*a_pVal++ != '<') return false; + return true; +} + +template +bool +CSimpleIniTempl::IsMultiLineData( + const SI_CHAR * a_pData + ) const +{ + // data is multi-line if it has any of the following features: + // * whitespace prefix + // * embedded newlines + // * whitespace suffix + + // empty string + if (!*a_pData) { + return false; + } + + // check for prefix + if (IsSpace(*a_pData)) { + return true; + } + + // embedded newlines + const SI_CHAR * pStart = a_pData; + while (*a_pData) { + if (IsNewLineChar(*a_pData)) { + return true; + } + ++a_pData; + } + + // check for suffix (ensure we don't go before start of string) + if (a_pData > pStart && IsSpace(*(a_pData - 1))) { + return true; + } + + return false; +} + +template +bool +CSimpleIniTempl::IsSingleLineQuotedValue( + const SI_CHAR* a_pData +) const +{ + // data needs quoting if it starts or ends with whitespace + // and doesn't have embedded newlines + + // empty string + if (!*a_pData) { + return false; + } + + // check for prefix + if (IsSpace(*a_pData)) { + return true; + } + + // embedded newlines + const SI_CHAR * pStart = a_pData; + while (*a_pData) { + if (IsNewLineChar(*a_pData)) { + return false; + } + ++a_pData; + } + + // check for suffix (ensure we don't go before start of string) + if (a_pData > pStart && IsSpace(*(a_pData - 1))) { + return true; + } + + return false; +} + +template +bool +CSimpleIniTempl::IsNewLineChar( + SI_CHAR a_c + ) const +{ + return (a_c == '\n' || a_c == '\r'); +} + +template +bool +CSimpleIniTempl::LoadMultiLineText( + SI_CHAR *& a_pData, + const SI_CHAR *& a_pVal, + const SI_CHAR * a_pTagName, + bool a_bAllowBlankLinesInComment + ) const +{ + // we modify this data to strip all newlines down to a single '\n' + // character. This means that on Windows we need to strip out some + // characters which will make the data shorter. + // i.e. LINE1-LINE1\r\nLINE2-LINE2\0 will become + // LINE1-LINE1\nLINE2-LINE2\0 + // The pDataLine entry is the pointer to the location in memory that + // the current line needs to start to run following the existing one. + // This may be the same as pCurrLine in which case no move is needed. + SI_CHAR * pDataLine = a_pData; + SI_CHAR * pCurrLine; + + // value starts at the current line + a_pVal = a_pData; + + // find the end tag. This tag must start in column 1 and be + // followed by a newline. We ignore any whitespace after the end + // tag but not whitespace before it. + SI_CHAR cEndOfLineChar = *a_pData; + for(;;) { + // if we are loading comments then we need a comment character as + // the first character on every line + if (!a_pTagName && !IsComment(*a_pData)) { + // if we aren't allowing blank lines then we're done + if (!a_bAllowBlankLinesInComment) { + break; + } + + // if we are allowing blank lines then we only include them + // in this comment if another comment follows, so read ahead + // to find out. + SI_CHAR * pCurr = a_pData; + int nNewLines = 0; + while (IsSpace(*pCurr)) { + if (IsNewLineChar(*pCurr)) { + ++nNewLines; + SkipNewLine(pCurr); + } + else { + ++pCurr; + } + } + + // we have a comment, add the blank lines to the output + // and continue processing from here + if (IsComment(*pCurr)) { + for (; nNewLines > 0; --nNewLines) *pDataLine++ = '\n'; + a_pData = pCurr; + continue; + } + + // the comment ends here + break; + } + + // find the end of this line + pCurrLine = a_pData; + while (*a_pData && !IsNewLineChar(*a_pData)) ++a_pData; + + // move this line down to the location that it should be if necessary + if (pDataLine < pCurrLine) { + size_t nLen = (size_t) (a_pData - pCurrLine); + memmove(pDataLine, pCurrLine, nLen * sizeof(SI_CHAR)); + pDataLine[nLen] = '\0'; + } + + // end the line with a NULL + cEndOfLineChar = *a_pData; + *a_pData = 0; + + // if are looking for a tag then do the check now. This is done before + // checking for end of the data, so that if we have the tag at the end + // of the data then the tag is removed correctly. + if (a_pTagName) { + // strip whitespace from the end of this tag + SI_CHAR* pc = a_pData - 1; + while (pc > pDataLine && IsSpace(*pc)) --pc; + SI_CHAR ch = *++pc; + *pc = 0; + + if (!IsLess(pDataLine, a_pTagName) && !IsLess(a_pTagName, pDataLine)) { + break; + } + + *pc = ch; + } + + // if we are at the end of the data then we just automatically end + // this entry and return the current data. + if (!cEndOfLineChar) { + return true; + } + + // otherwise we need to process this newline to ensure that it consists + // of just a single \n character. + pDataLine += (a_pData - pCurrLine); + *a_pData = cEndOfLineChar; + SkipNewLine(a_pData); + *pDataLine++ = '\n'; + } + + // if we didn't find a comment at all then return false + if (a_pVal == a_pData) { + a_pVal = NULL; + return false; + } + + // the data (which ends at the end of the last line) needs to be + // null-terminated BEFORE before the newline character(s). If the + // user wants a new line in the multi-line data then they need to + // add an empty line before the tag. + *--pDataLine = '\0'; + + // if looking for a tag and if we aren't at the end of the data, + // then move a_pData to the start of the next line. + if (a_pTagName && cEndOfLineChar) { + SI_ASSERT(IsNewLineChar(cEndOfLineChar)); + *a_pData = cEndOfLineChar; + SkipNewLine(a_pData); + } + + return true; +} + +template +SI_Error +CSimpleIniTempl::CopyString( + const SI_CHAR *& a_pString + ) +{ + size_t uLen = 0; + if (sizeof(SI_CHAR) == sizeof(char)) { + uLen = strlen((const char *)a_pString); + } + else if (sizeof(SI_CHAR) == sizeof(wchar_t)) { + uLen = wcslen((const wchar_t *)a_pString); + } + else { + for ( ; a_pString[uLen]; ++uLen) /*loop*/ ; + } + ++uLen; // NULL character + SI_CHAR * pCopy = new(std::nothrow) SI_CHAR[uLen]; + if (!pCopy) { + return SI_NOMEM; + } + memcpy(pCopy, a_pString, sizeof(SI_CHAR)*uLen); + m_strings.push_back(pCopy); + a_pString = pCopy; + return SI_OK; +} + +template +SI_Error +CSimpleIniTempl::AddEntry( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + const SI_CHAR * a_pValue, + const SI_CHAR * a_pComment, + bool a_bForceReplace, + bool a_bCopyStrings + ) +{ + SI_Error rc; + bool bInserted = false; + + SI_ASSERT(!a_pComment || IsComment(*a_pComment)); + + // if we are copying strings then make a copy of the comment now + // because we will need it when we add the entry. + if (a_bCopyStrings && a_pComment) { + rc = CopyString(a_pComment); + if (rc < 0) return rc; + } + + // create the section entry if necessary + typename TSection::iterator iSection = m_data.find(a_pSection); + if (iSection == m_data.end()) { + // if the section doesn't exist then we need a copy as the + // string needs to last beyond the end of this function + if (a_bCopyStrings) { + rc = CopyString(a_pSection); + if (rc < 0) return rc; + } + + // only set the comment if this is a section only entry + Entry oSection(a_pSection, ++m_nOrder); + if (a_pComment && !a_pKey) { + oSection.pComment = a_pComment; + } + + typename TSection::value_type oEntry(oSection, TKeyVal()); + typedef typename TSection::iterator SectionIterator; + std::pair i = m_data.insert(oEntry); + iSection = i.first; + bInserted = true; + } + if (!a_pKey) { + // section only entries are specified with pItem as NULL + return bInserted ? SI_INSERTED : SI_UPDATED; + } + + // check for existence of the key + TKeyVal & keyval = iSection->second; + typename TKeyVal::iterator iKey = keyval.find(a_pKey); + bInserted = iKey == keyval.end(); + + // remove all existing entries but save the load order and + // comment of the first entry + int nLoadOrder = ++m_nOrder; + if (iKey != keyval.end() && m_bAllowMultiKey && a_bForceReplace) { + const SI_CHAR * pComment = NULL; + while (iKey != keyval.end() && !IsLess(a_pKey, iKey->first.pItem)) { + if (iKey->first.nOrder < nLoadOrder) { + nLoadOrder = iKey->first.nOrder; + pComment = iKey->first.pComment; + } + ++iKey; + } + if (pComment) { + DeleteString(a_pComment); + a_pComment = pComment; + rc = CopyString(a_pComment); + if (rc < 0) return rc; + } + Delete(a_pSection, a_pKey); + iKey = keyval.end(); + } + + // values need to be a valid string, even if they are an empty string + if (!a_pValue) { + a_pValue = &m_cEmptyString; + } + + // make string copies if necessary + bool bForceCreateNewKey = m_bAllowMultiKey && !a_bForceReplace; + if (a_bCopyStrings) { + if (bForceCreateNewKey || iKey == keyval.end()) { + // if the key doesn't exist then we need a copy as the + // string needs to last beyond the end of this function + // because we will be inserting the key next + rc = CopyString(a_pKey); + if (rc < 0) return rc; + } + + // we always need a copy of the value + rc = CopyString(a_pValue); + if (rc < 0) return rc; + } + + // create the key entry + if (iKey == keyval.end() || bForceCreateNewKey) { + Entry oKey(a_pKey, nLoadOrder); + if (a_pComment) { + oKey.pComment = a_pComment; + } + typename TKeyVal::value_type oEntry(oKey, static_cast(NULL)); + iKey = keyval.insert(oEntry); + } + + iKey->second = a_pValue; + return bInserted ? SI_INSERTED : SI_UPDATED; +} + +template +const SI_CHAR * +CSimpleIniTempl::GetValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + const SI_CHAR * a_pDefault, + bool * a_pHasMultiple + ) const +{ + if (a_pHasMultiple) { + *a_pHasMultiple = false; + } + if (!a_pSection || !a_pKey) { + return a_pDefault; + } + typename TSection::const_iterator iSection = m_data.find(a_pSection); + if (iSection == m_data.end()) { + return a_pDefault; + } + typename TKeyVal::const_iterator iKeyVal = iSection->second.find(a_pKey); + if (iKeyVal == iSection->second.end()) { + return a_pDefault; + } + + // check for multiple entries with the same key + if (m_bAllowMultiKey && a_pHasMultiple) { + typename TKeyVal::const_iterator iTemp = iKeyVal; + if (++iTemp != iSection->second.end()) { + if (!IsLess(a_pKey, iTemp->first.pItem)) { + *a_pHasMultiple = true; + } + } + } + + return iKeyVal->second; +} + +template +long +CSimpleIniTempl::GetLongValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + long a_nDefault, + bool * a_pHasMultiple + ) const +{ + // return the default if we don't have a value + const SI_CHAR * pszValue = GetValue(a_pSection, a_pKey, NULL, a_pHasMultiple); + if (!pszValue || !*pszValue) return a_nDefault; + + // convert to UTF-8/MBCS which for a numeric value will be the same as ASCII + char szValue[64] = { 0 }; + SI_CONVERTER c(m_bStoreIsUtf8); + if (!c.ConvertToStore(pszValue, szValue, sizeof(szValue))) { + return a_nDefault; + } + + // handle the value as hex if prefaced with "0x" + long nValue = a_nDefault; + char * pszSuffix = szValue; + if (szValue[0] == '0' && (szValue[1] == 'x' || szValue[1] == 'X')) { + if (!szValue[2]) return a_nDefault; + nValue = strtol(&szValue[2], &pszSuffix, 16); + } + else { + nValue = strtol(szValue, &pszSuffix, 10); + } + + // any invalid strings will return the default value + if (*pszSuffix) { + return a_nDefault; + } + + return nValue; +} + +template +SI_Error +CSimpleIniTempl::SetLongValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + long a_nValue, + const SI_CHAR * a_pComment, + bool a_bUseHex, + bool a_bForceReplace + ) +{ + // use SetValue to create sections + if (!a_pSection || !a_pKey) return SI_FAIL; + + // convert to an ASCII string + char szInput[64]; +#if __STDC_WANT_SECURE_LIB__ && !_WIN32_WCE + sprintf_s(szInput, a_bUseHex ? "0x%lx" : "%ld", a_nValue); +#else // !__STDC_WANT_SECURE_LIB__ + snprintf(szInput, sizeof(szInput), a_bUseHex ? "0x%lx" : "%ld", a_nValue); +#endif // __STDC_WANT_SECURE_LIB__ + + // convert to output text + SI_CHAR szOutput[64]; + SI_CONVERTER c(m_bStoreIsUtf8); + c.ConvertFromStore(szInput, strlen(szInput) + 1, + szOutput, sizeof(szOutput) / sizeof(SI_CHAR)); + + // actually add it + return AddEntry(a_pSection, a_pKey, szOutput, a_pComment, a_bForceReplace, true); +} + +template +double +CSimpleIniTempl::GetDoubleValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + double a_nDefault, + bool * a_pHasMultiple + ) const +{ + // return the default if we don't have a value + const SI_CHAR * pszValue = GetValue(a_pSection, a_pKey, NULL, a_pHasMultiple); + if (!pszValue || !*pszValue) return a_nDefault; + + // convert to UTF-8/MBCS which for a numeric value will be the same as ASCII + char szValue[64] = { 0 }; + SI_CONVERTER c(m_bStoreIsUtf8); + if (!c.ConvertToStore(pszValue, szValue, sizeof(szValue))) { + return a_nDefault; + } + + char * pszSuffix = szValue; + double nValue = strtod(szValue, &pszSuffix); + + // any invalid strings will return the default value + // check if no conversion was performed or if there are trailing characters + if (pszSuffix == szValue || *pszSuffix) { + return a_nDefault; + } + + return nValue; +} + +template +SI_Error +CSimpleIniTempl::SetDoubleValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + double a_nValue, + const SI_CHAR * a_pComment, + bool a_bForceReplace + ) +{ + // use SetValue to create sections + if (!a_pSection || !a_pKey) return SI_FAIL; + + // convert to an ASCII string + char szInput[64]; +#if __STDC_WANT_SECURE_LIB__ && !_WIN32_WCE + sprintf_s(szInput, "%f", a_nValue); +#else // !__STDC_WANT_SECURE_LIB__ + snprintf(szInput, sizeof(szInput), "%f", a_nValue); +#endif // __STDC_WANT_SECURE_LIB__ + + // convert to output text + SI_CHAR szOutput[64]; + SI_CONVERTER c(m_bStoreIsUtf8); + c.ConvertFromStore(szInput, strlen(szInput) + 1, + szOutput, sizeof(szOutput) / sizeof(SI_CHAR)); + + // actually add it + return AddEntry(a_pSection, a_pKey, szOutput, a_pComment, a_bForceReplace, true); +} + +template +bool +CSimpleIniTempl::GetBoolValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + bool a_bDefault, + bool * a_pHasMultiple + ) const +{ + // return the default if we don't have a value + const SI_CHAR * pszValue = GetValue(a_pSection, a_pKey, NULL, a_pHasMultiple); + if (!pszValue || !*pszValue) return a_bDefault; + + // we only look at the minimum number of characters + switch (pszValue[0]) { + case 't': case 'T': // true + case 'y': case 'Y': // yes + case '1': // 1 (one) + return true; + + case 'f': case 'F': // false + case 'n': case 'N': // no + case '0': // 0 (zero) + return false; + + case 'o': case 'O': + if (pszValue[1] == 'n' || pszValue[1] == 'N') return true; // on + if (pszValue[1] == 'f' || pszValue[1] == 'F') return false; // off + break; + } + + // no recognized value, return the default + return a_bDefault; +} + +template +SI_Error +CSimpleIniTempl::SetBoolValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + bool a_bValue, + const SI_CHAR * a_pComment, + bool a_bForceReplace + ) +{ + // use SetValue to create sections + if (!a_pSection || !a_pKey) return SI_FAIL; + + // convert to an ASCII string + const char * pszInput = a_bValue ? "true" : "false"; + + // convert to output text + SI_CHAR szOutput[64]; + SI_CONVERTER c(m_bStoreIsUtf8); + c.ConvertFromStore(pszInput, strlen(pszInput) + 1, + szOutput, sizeof(szOutput) / sizeof(SI_CHAR)); + + // actually add it + return AddEntry(a_pSection, a_pKey, szOutput, a_pComment, a_bForceReplace, true); +} + +template +bool +CSimpleIniTempl::GetAllValues( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + TNamesDepend & a_values + ) const +{ + a_values.clear(); + + if (!a_pSection || !a_pKey) { + return false; + } + typename TSection::const_iterator iSection = m_data.find(a_pSection); + if (iSection == m_data.end()) { + return false; + } + typename TKeyVal::const_iterator iKeyVal = iSection->second.find(a_pKey); + if (iKeyVal == iSection->second.end()) { + return false; + } + + // insert all values for this key + a_values.push_back(Entry(iKeyVal->second, iKeyVal->first.pComment, iKeyVal->first.nOrder)); + if (m_bAllowMultiKey) { + ++iKeyVal; + while (iKeyVal != iSection->second.end() && !IsLess(a_pKey, iKeyVal->first.pItem)) { + a_values.push_back(Entry(iKeyVal->second, iKeyVal->first.pComment, iKeyVal->first.nOrder)); + ++iKeyVal; + } + } + + return true; +} + +template +int +CSimpleIniTempl::GetSectionSize( + const SI_CHAR * a_pSection + ) const +{ + if (!a_pSection) { + return -1; + } + + typename TSection::const_iterator iSection = m_data.find(a_pSection); + if (iSection == m_data.end()) { + return -1; + } + const TKeyVal & section = iSection->second; + + // if multi-key isn't permitted then the section size is + // the number of keys that we have. + if (!m_bAllowMultiKey || section.empty()) { + return (int) section.size(); + } + + // otherwise we need to count them + int nCount = 0; + const SI_CHAR * pLastKey = NULL; + typename TKeyVal::const_iterator iKeyVal = section.begin(); + for (; iKeyVal != section.end(); ++iKeyVal) { + if (!pLastKey || IsLess(pLastKey, iKeyVal->first.pItem)) { + ++nCount; + pLastKey = iKeyVal->first.pItem; + } + } + return nCount; +} + +template +const typename CSimpleIniTempl::TKeyVal * +CSimpleIniTempl::GetSection( + const SI_CHAR * a_pSection + ) const +{ + if (a_pSection) { + typename TSection::const_iterator i = m_data.find(a_pSection); + if (i != m_data.end()) { + return &(i->second); + } + } + return 0; +} + +template +void +CSimpleIniTempl::GetAllSections( + TNamesDepend & a_names + ) const +{ + a_names.clear(); + typename TSection::const_iterator i = m_data.begin(); + for (; i != m_data.end(); ++i) { + a_names.push_back(i->first); + } +} + +template +bool +CSimpleIniTempl::GetAllKeys( + const SI_CHAR * a_pSection, + TNamesDepend & a_names + ) const +{ + a_names.clear(); + + if (!a_pSection) { + return false; + } + + typename TSection::const_iterator iSection = m_data.find(a_pSection); + if (iSection == m_data.end()) { + return false; + } + + const TKeyVal & section = iSection->second; + const SI_CHAR * pLastKey = NULL; + typename TKeyVal::const_iterator iKeyVal = section.begin(); + for (; iKeyVal != section.end(); ++iKeyVal) { + if (!pLastKey || IsLess(pLastKey, iKeyVal->first.pItem)) { + a_names.push_back(iKeyVal->first); + pLastKey = iKeyVal->first.pItem; + } + } + + return true; +} + +template +SI_Error +CSimpleIniTempl::SaveFile( + const char * a_pszFile, + bool a_bAddSignature + ) const +{ + FILE * fp = NULL; +#if __STDC_WANT_SECURE_LIB__ && !_WIN32_WCE + fopen_s(&fp, a_pszFile, "wb"); +#else // !__STDC_WANT_SECURE_LIB__ + fp = fopen(a_pszFile, "wb"); +#endif // __STDC_WANT_SECURE_LIB__ + if (!fp) return SI_FILE; + SI_Error rc = SaveFile(fp, a_bAddSignature); + fclose(fp); + return rc; +} + +#ifdef SI_HAS_WIDE_FILE +template +SI_Error +CSimpleIniTempl::SaveFile( + const SI_WCHAR_T * a_pwszFile, + bool a_bAddSignature + ) const +{ +#ifdef _WIN32 + FILE * fp = NULL; +#if __STDC_WANT_SECURE_LIB__ && !_WIN32_WCE + _wfopen_s(&fp, a_pwszFile, L"wb"); +#else // !__STDC_WANT_SECURE_LIB__ + fp = _wfopen(a_pwszFile, L"wb"); +#endif // __STDC_WANT_SECURE_LIB__ + if (!fp) return SI_FILE; + SI_Error rc = SaveFile(fp, a_bAddSignature); + fclose(fp); + return rc; +#else // !_WIN32 (therefore SI_CONVERT_ICU) + char szFile[256]; + u_austrncpy(szFile, a_pwszFile, sizeof(szFile)); + return SaveFile(szFile, a_bAddSignature); +#endif // _WIN32 +} +#endif // SI_HAS_WIDE_FILE + +template +SI_Error +CSimpleIniTempl::SaveFile( + FILE * a_pFile, + bool a_bAddSignature + ) const +{ + FileWriter writer(a_pFile); + return Save(writer, a_bAddSignature); +} + +template +SI_Error +CSimpleIniTempl::Save( + OutputWriter & a_oOutput, + bool a_bAddSignature + ) const +{ + Converter convert(m_bStoreIsUtf8); + + // add the UTF-8 signature if it is desired + if (m_bStoreIsUtf8 && a_bAddSignature) { + a_oOutput.Write(SI_UTF8_SIGNATURE); + } + + // get all of the sections sorted in load order + TNamesDepend oSections; + GetAllSections(oSections); +#if defined(_MSC_VER) && _MSC_VER <= 1200 + oSections.sort(); +#elif defined(__BORLANDC__) + oSections.sort(Entry::LoadOrder()); +#else + oSections.sort(typename Entry::LoadOrder()); +#endif + + // if there is an empty section name, then it must be written out first + // regardless of the load order + typename TNamesDepend::iterator is = oSections.begin(); + for (; is != oSections.end(); ++is) { + if (!*is->pItem) { + // move the empty section name to the front of the section list + if (is != oSections.begin()) { + oSections.splice(oSections.begin(), oSections, is, std::next(is)); + } + break; + } + } + + // write the file comment if we have one + bool bNeedNewLine = false; + if (m_pFileComment) { + if (!OutputMultiLineText(a_oOutput, convert, m_pFileComment)) { + return SI_FAIL; + } + bNeedNewLine = true; + } + + // iterate through our sections and output the data + typename TNamesDepend::const_iterator iSection = oSections.begin(); + for ( ; iSection != oSections.end(); ++iSection ) { + // write out the comment if there is one + if (iSection->pComment) { + if (bNeedNewLine) { + a_oOutput.Write(SI_NEWLINE_A); + a_oOutput.Write(SI_NEWLINE_A); + } + if (!OutputMultiLineText(a_oOutput, convert, iSection->pComment)) { + return SI_FAIL; + } + bNeedNewLine = false; + } + + if (bNeedNewLine) { + a_oOutput.Write(SI_NEWLINE_A); + a_oOutput.Write(SI_NEWLINE_A); + bNeedNewLine = false; + } + + // write the section (unless there is no section name) + if (*iSection->pItem) { + if (!convert.ConvertToStore(iSection->pItem)) { + return SI_FAIL; + } + a_oOutput.Write("["); + a_oOutput.Write(convert.Data()); + a_oOutput.Write("]"); + a_oOutput.Write(SI_NEWLINE_A); + } + + // get all of the keys sorted in load order + TNamesDepend oKeys; + GetAllKeys(iSection->pItem, oKeys); +#if defined(_MSC_VER) && _MSC_VER <= 1200 + oKeys.sort(); +#elif defined(__BORLANDC__) + oKeys.sort(Entry::LoadOrder()); +#else + oKeys.sort(typename Entry::LoadOrder()); +#endif + + // write all keys and values + typename TNamesDepend::const_iterator iKey = oKeys.begin(); + for ( ; iKey != oKeys.end(); ++iKey) { + // get all values for this key + TNamesDepend oValues; + GetAllValues(iSection->pItem, iKey->pItem, oValues); + + typename TNamesDepend::const_iterator iValue = oValues.begin(); + for ( ; iValue != oValues.end(); ++iValue) { + // write out the comment if there is one + if (iValue->pComment) { + a_oOutput.Write(SI_NEWLINE_A); + if (!OutputMultiLineText(a_oOutput, convert, iValue->pComment)) { + return SI_FAIL; + } + } + + // write the key + if (!convert.ConvertToStore(iKey->pItem)) { + return SI_FAIL; + } + a_oOutput.Write(convert.Data()); + + // write the value as long + if (*iValue->pItem || !m_bAllowKeyOnly) { + if (!convert.ConvertToStore(iValue->pItem)) { + return SI_FAIL; + } + a_oOutput.Write(m_bSpaces ? " = " : "="); + if (m_bParseQuotes && IsSingleLineQuotedValue(iValue->pItem)) { + // the only way to preserve external whitespace on a value (i.e. before or after) + // is to quote it. This is simple quoting, we don't escape quotes within the data. + a_oOutput.Write("\""); + a_oOutput.Write(convert.Data()); + a_oOutput.Write("\""); + } + else if (m_bAllowMultiLine && IsMultiLineData(iValue->pItem)) { + // multi-line data needs to be processed specially to ensure + // that we use the correct newline format for the current system + a_oOutput.Write("<<pItem)) { + return SI_FAIL; + } + a_oOutput.Write("END_OF_TEXT"); + } + else { + a_oOutput.Write(convert.Data()); + } + } + a_oOutput.Write(SI_NEWLINE_A); + } + } + + bNeedNewLine = true; + } + + return SI_OK; +} + +template +bool +CSimpleIniTempl::OutputMultiLineText( + OutputWriter & a_oOutput, + Converter & a_oConverter, + const SI_CHAR * a_pText + ) const +{ + const SI_CHAR * pEndOfLine; + SI_CHAR cEndOfLineChar = *a_pText; + while (cEndOfLineChar) { + // find the end of this line + pEndOfLine = a_pText; + for (; *pEndOfLine && *pEndOfLine != '\n'; ++pEndOfLine) /*loop*/ ; + cEndOfLineChar = *pEndOfLine; + + // temporarily null terminate, convert and output the line + *const_cast(pEndOfLine) = 0; + if (!a_oConverter.ConvertToStore(a_pText)) { + return false; + } + *const_cast(pEndOfLine) = cEndOfLineChar; + a_pText += (pEndOfLine - a_pText) + 1; + a_oOutput.Write(a_oConverter.Data()); + a_oOutput.Write(SI_NEWLINE_A); + } + return true; +} + +template +bool +CSimpleIniTempl::Delete( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + bool a_bRemoveEmpty + ) +{ + return DeleteValue(a_pSection, a_pKey, NULL, a_bRemoveEmpty); +} + +template +bool +CSimpleIniTempl::DeleteValue( + const SI_CHAR * a_pSection, + const SI_CHAR * a_pKey, + const SI_CHAR * a_pValue, + bool a_bRemoveEmpty + ) +{ + if (!a_pSection) { + return false; + } + + typename TSection::iterator iSection = m_data.find(a_pSection); + if (iSection == m_data.end()) { + return false; + } + + // remove a single key if we have a keyname + if (a_pKey) { + typename TKeyVal::iterator iKeyVal = iSection->second.find(a_pKey); + if (iKeyVal == iSection->second.end()) { + return false; + } + + const static SI_STRLESS isLess = SI_STRLESS(); + + // remove any copied strings and then the key + typename TKeyVal::iterator iDelete; + bool bDeleted = false; + do { + iDelete = iKeyVal++; + + if(a_pValue == NULL || + (isLess(a_pValue, iDelete->second) == false && + isLess(iDelete->second, a_pValue) == false)) { + DeleteString(iDelete->first.pItem); + DeleteString(iDelete->second); + iSection->second.erase(iDelete); + bDeleted = true; + } + } + while (iKeyVal != iSection->second.end() + && !IsLess(a_pKey, iKeyVal->first.pItem)); + + if(!bDeleted) { + return false; + } + + // done now if the section is not empty or we are not pruning away + // the empty sections. Otherwise let it fall through into the section + // deletion code + if (!a_bRemoveEmpty || !iSection->second.empty()) { + return true; + } + } + else { + // delete all copied strings from this section. The actual + // entries will be removed when the section is removed. + typename TKeyVal::iterator iKeyVal = iSection->second.begin(); + for ( ; iKeyVal != iSection->second.end(); ++iKeyVal) { + DeleteString(iKeyVal->first.pItem); + DeleteString(iKeyVal->second); + } + } + + // delete the section itself + DeleteString(iSection->first.pItem); + m_data.erase(iSection); + + return true; +} + +template +void +CSimpleIniTempl::DeleteString( + const SI_CHAR * a_pString + ) +{ + // strings may exist either inside the data block, or they will be + // individually allocated and stored in m_strings. We only physically + // delete those stored in m_strings. + if (!m_pData || a_pString < m_pData || a_pString >= m_pData + m_uDataLen) { + typename TNamesDepend::iterator i = m_strings.begin(); + for (;i != m_strings.end(); ++i) { + if (a_pString == i->pItem) { + delete[] const_cast(i->pItem); + m_strings.erase(i); + break; + } + } + } +} + +// --------------------------------------------------------------------------- +// CONVERSION FUNCTIONS +// --------------------------------------------------------------------------- + +// Defines the conversion classes for different libraries. Before including +// SimpleIni.h, set the converter that you wish you use by defining one of the +// following symbols. +// +// SI_NO_CONVERSION Do not make the "W" wide character version of the +// library available. Only CSimpleIniA etc is defined. +// Default on Linux/MacOS/etc. +// SI_CONVERT_WIN32 Use the Win32 API functions for conversion. +// Default on Windows. +// SI_CONVERT_GENERIC Use the Unicode reference conversion library in +// the accompanying files ConvertUTF.h/c +// SI_CONVERT_ICU Use the IBM ICU conversion library. Requires +// ICU headers on include path and icuuc.lib + +#if !defined(SI_NO_CONVERSION) && !defined(SI_CONVERT_GENERIC) && !defined(SI_CONVERT_WIN32) && !defined(SI_CONVERT_ICU) +# ifdef _WIN32 +# define SI_CONVERT_WIN32 +# else +# define SI_NO_CONVERSION +# endif +#endif + +/** + * Generic case-sensitive less than comparison. This class returns numerically + * ordered ASCII case-sensitive text for all possible sizes and types of + * SI_CHAR. + */ +template +struct SI_GenericCase { + bool operator()(const SI_CHAR * pLeft, const SI_CHAR * pRight) const { + long cmp; + for ( ;*pLeft && *pRight; ++pLeft, ++pRight) { + cmp = (long) *pLeft - (long) *pRight; + if (cmp != 0) { + return cmp < 0; + } + } + return *pRight != 0; + } +}; + +/** + * Generic ASCII case-insensitive less than comparison. This class returns + * numerically ordered ASCII case-insensitive text for all possible sizes + * and types of SI_CHAR. It is not safe for MBCS text comparison where + * ASCII A-Z characters are used in the encoding of multi-byte characters. + */ +template +struct SI_GenericNoCase { + inline SI_CHAR locase(SI_CHAR ch) const { + return (ch < 'A' || ch > 'Z') ? ch : (ch - 'A' + 'a'); + } + bool operator()(const SI_CHAR * pLeft, const SI_CHAR * pRight) const { + long cmp; + for ( ;*pLeft && *pRight; ++pLeft, ++pRight) { + cmp = (long) locase(*pLeft) - (long) locase(*pRight); + if (cmp != 0) { + return cmp < 0; + } + } + return *pRight != 0; + } +}; + +/** + * Null conversion class for MBCS/UTF-8 to char (or equivalent). + */ +template +class SI_ConvertA { + bool m_bStoreIsUtf8; +protected: + SI_ConvertA() { } +public: + SI_ConvertA(bool a_bStoreIsUtf8) : m_bStoreIsUtf8(a_bStoreIsUtf8) { } + + /* copy and assignment */ + SI_ConvertA(const SI_ConvertA & rhs) { operator=(rhs); } + SI_ConvertA & operator=(const SI_ConvertA & rhs) { + m_bStoreIsUtf8 = rhs.m_bStoreIsUtf8; + return *this; + } + + /** Calculate the number of SI_CHAR required for converting the input + * from the storage format. The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData Data in storage format to be converted to SI_CHAR. + * @param a_uInputDataLen Length of storage format data in bytes. This + * must be the actual length of the data, including + * NULL byte if NULL terminated string is required. + * @return Number of SI_CHAR required by the string when + * converted. If there are embedded NULL bytes in the + * input data, only the string up and not including + * the NULL byte will be converted. + * @return -1 cast to size_t on a conversion error. + */ + size_t SizeFromStore( + const char * a_pInputData, + size_t a_uInputDataLen) + { + (void)a_pInputData; + SI_ASSERT(a_uInputDataLen != (size_t) -1); + + // ASCII/MBCS/UTF-8 needs no conversion + return a_uInputDataLen; + } + + /** Convert the input string from the storage format to SI_CHAR. + * The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData Data in storage format to be converted to SI_CHAR. + * @param a_uInputDataLen Length of storage format data in bytes. This + * must be the actual length of the data, including + * NULL byte if NULL terminated string is required. + * @param a_pOutputData Pointer to the output buffer to received the + * converted data. + * @param a_uOutputDataSize Size of the output buffer in SI_CHAR. + * @return true if all of the input data was successfully + * converted. + */ + bool ConvertFromStore( + const char * a_pInputData, + size_t a_uInputDataLen, + SI_CHAR * a_pOutputData, + size_t a_uOutputDataSize) + { + // ASCII/MBCS/UTF-8 needs no conversion + if (a_uInputDataLen > a_uOutputDataSize) { + return false; + } + memcpy(a_pOutputData, a_pInputData, a_uInputDataLen); + return true; + } + + /** Calculate the number of char required by the storage format of this + * data. The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData NULL terminated string to calculate the number of + * bytes required to be converted to storage format. + * @return Number of bytes required by the string when + * converted to storage format. This size always + * includes space for the terminating NULL character. + * @return -1 cast to size_t on a conversion error. + */ + size_t SizeToStore( + const SI_CHAR * a_pInputData) + { + // ASCII/MBCS/UTF-8 needs no conversion + return strlen((const char *)a_pInputData) + 1; + } + + /** Convert the input string to the storage format of this data. + * The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData NULL terminated source string to convert. All of + * the data will be converted including the + * terminating NULL character. + * @param a_pOutputData Pointer to the buffer to receive the converted + * string. + * @param a_uOutputDataSize Size of the output buffer in char. + * @return true if all of the input data, including the + * terminating NULL character was successfully + * converted. + */ + bool ConvertToStore( + const SI_CHAR * a_pInputData, + char * a_pOutputData, + size_t a_uOutputDataSize) + { + // calc input string length (SI_CHAR type and size independent) + size_t uInputLen = strlen((const char *)a_pInputData) + 1; + if (uInputLen > a_uOutputDataSize) { + return false; + } + + // ascii/UTF-8 needs no conversion + memcpy(a_pOutputData, a_pInputData, uInputLen); + return true; + } +}; + + +// --------------------------------------------------------------------------- +// SI_CONVERT_GENERIC +// --------------------------------------------------------------------------- +#ifdef SI_CONVERT_GENERIC + +#define SI_Case SI_GenericCase +#define SI_NoCase SI_GenericNoCase + +#include +#include "ConvertUTF.h" + +/** + * Converts UTF-8 to a wchar_t (or equivalent) using the Unicode reference + * library functions. This can be used on all platforms. + */ +template +class SI_ConvertW { + bool m_bStoreIsUtf8; +protected: + SI_ConvertW() { } +public: + SI_ConvertW(bool a_bStoreIsUtf8) : m_bStoreIsUtf8(a_bStoreIsUtf8) { } + + /* copy and assignment */ + SI_ConvertW(const SI_ConvertW & rhs) { operator=(rhs); } + SI_ConvertW & operator=(const SI_ConvertW & rhs) { + m_bStoreIsUtf8 = rhs.m_bStoreIsUtf8; + return *this; + } + + /** Calculate the number of SI_CHAR required for converting the input + * from the storage format. The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData Data in storage format to be converted to SI_CHAR. + * @param a_uInputDataLen Length of storage format data in bytes. This + * must be the actual length of the data, including + * NULL byte if NULL terminated string is required. + * @return Number of SI_CHAR required by the string when + * converted. If there are embedded NULL bytes in the + * input data, only the string up and not including + * the NULL byte will be converted. + * @return -1 cast to size_t on a conversion error. + */ + size_t SizeFromStore( + const char * a_pInputData, + size_t a_uInputDataLen) + { + SI_ASSERT(a_uInputDataLen != (size_t) -1); + + if (m_bStoreIsUtf8) { + // worst case scenario for UTF-8 to wchar_t is 1 char -> 1 wchar_t + // so we just return the same number of characters required as for + // the source text. + return a_uInputDataLen; + } + + // get the required buffer size +#if defined(_MSC_VER) + size_t uBufSiz; + errno_t e = mbstowcs_s(&uBufSiz, NULL, 0, a_pInputData, a_uInputDataLen); + return (e == 0) ? uBufSiz : (size_t) -1; +#elif !defined(SI_NO_MBSTOWCS_NULL) + return mbstowcs(NULL, a_pInputData, a_uInputDataLen); +#else + // fall back processing for platforms that don't support a NULL dest to mbstowcs + // worst case scenario is 1:1, this will be a sufficient buffer size + (void)a_pInputData; + return a_uInputDataLen; +#endif + } + + /** Convert the input string from the storage format to SI_CHAR. + * The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData Data in storage format to be converted to SI_CHAR. + * @param a_uInputDataLen Length of storage format data in bytes. This + * must be the actual length of the data, including + * NULL byte if NULL terminated string is required. + * @param a_pOutputData Pointer to the output buffer to received the + * converted data. + * @param a_uOutputDataSize Size of the output buffer in SI_CHAR. + * @return true if all of the input data was successfully + * converted. + */ + bool ConvertFromStore( + const char * a_pInputData, + size_t a_uInputDataLen, + SI_CHAR * a_pOutputData, + size_t a_uOutputDataSize) + { + if (m_bStoreIsUtf8) { + // This uses the Unicode reference implementation to do the + // conversion from UTF-8 to wchar_t. The required files are + // ConvertUTF.h and ConvertUTF.c which should be included in + // the distribution but are publicly available from unicode.org + // at http://www.unicode.org/Public/PROGRAMS/CVTUTF/ + ConversionResult retval; + const UTF8 * pUtf8 = (const UTF8 *) a_pInputData; + if (sizeof(wchar_t) == sizeof(UTF32)) { + UTF32 * pUtf32 = (UTF32 *) a_pOutputData; + retval = ConvertUTF8toUTF32( + &pUtf8, pUtf8 + a_uInputDataLen, + &pUtf32, pUtf32 + a_uOutputDataSize, + lenientConversion); + } + else if (sizeof(wchar_t) == sizeof(UTF16)) { + UTF16 * pUtf16 = (UTF16 *) a_pOutputData; + retval = ConvertUTF8toUTF16( + &pUtf8, pUtf8 + a_uInputDataLen, + &pUtf16, pUtf16 + a_uOutputDataSize, + lenientConversion); + } + return retval == conversionOK; + } + + // convert to wchar_t +#if defined(_MSC_VER) + size_t uBufSiz; + errno_t e = mbstowcs_s(&uBufSiz, + a_pOutputData, a_uOutputDataSize, + a_pInputData, a_uInputDataLen); + (void)uBufSiz; + return (e == 0); +#else + size_t retval = mbstowcs(a_pOutputData, + a_pInputData, a_uOutputDataSize); + return retval != (size_t)(-1); +#endif + } + + /** Calculate the number of char required by the storage format of this + * data. The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData NULL terminated string to calculate the number of + * bytes required to be converted to storage format. + * @return Number of bytes required by the string when + * converted to storage format. This size always + * includes space for the terminating NULL character. + * @return -1 cast to size_t on a conversion error. + */ + size_t SizeToStore( + const SI_CHAR * a_pInputData) + { + if (m_bStoreIsUtf8) { + // worst case scenario for wchar_t to UTF-8 is 1 wchar_t -> 6 char + size_t uLen = 0; + while (a_pInputData[uLen]) { + ++uLen; + } + return (6 * uLen) + 1; + } + else { + size_t uLen = wcstombs(NULL, a_pInputData, 0); + if (uLen == (size_t)(-1)) { + return uLen; + } + return uLen + 1; // include NULL terminator + } + } + + /** Convert the input string to the storage format of this data. + * The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData NULL terminated source string to convert. All of + * the data will be converted including the + * terminating NULL character. + * @param a_pOutputData Pointer to the buffer to receive the converted + * string. + * @param a_uOutputDataSize Size of the output buffer in char. + * @return true if all of the input data, including the + * terminating NULL character was successfully + * converted. + */ + bool ConvertToStore( + const SI_CHAR * a_pInputData, + char * a_pOutputData, + size_t a_uOutputDataSize + ) + { + if (m_bStoreIsUtf8) { + // calc input string length (SI_CHAR type and size independent) + size_t uInputLen = 0; + while (a_pInputData[uInputLen]) { + ++uInputLen; + } + ++uInputLen; // include the NULL char + + // This uses the Unicode reference implementation to do the + // conversion from wchar_t to UTF-8. The required files are + // ConvertUTF.h and ConvertUTF.c which should be included in + // the distribution but are publicly available from unicode.org + // at http://www.unicode.org/Public/PROGRAMS/CVTUTF/ + ConversionResult retval; + UTF8 * pUtf8 = (UTF8 *) a_pOutputData; + if (sizeof(wchar_t) == sizeof(UTF32)) { + const UTF32 * pUtf32 = (const UTF32 *) a_pInputData; + retval = ConvertUTF32toUTF8( + &pUtf32, pUtf32 + uInputLen, + &pUtf8, pUtf8 + a_uOutputDataSize, + lenientConversion); + } + else if (sizeof(wchar_t) == sizeof(UTF16)) { + const UTF16 * pUtf16 = (const UTF16 *) a_pInputData; + retval = ConvertUTF16toUTF8( + &pUtf16, pUtf16 + uInputLen, + &pUtf8, pUtf8 + a_uOutputDataSize, + lenientConversion); + } + return retval == conversionOK; + } + else { + size_t retval = wcstombs(a_pOutputData, + a_pInputData, a_uOutputDataSize); + return retval != (size_t) -1; + } + } +}; + +#endif // SI_CONVERT_GENERIC + + +// --------------------------------------------------------------------------- +// SI_CONVERT_ICU +// --------------------------------------------------------------------------- +#ifdef SI_CONVERT_ICU + +#define SI_Case SI_GenericCase +#define SI_NoCase SI_GenericNoCase + +#include + +/** + * Converts MBCS/UTF-8 to UChar using ICU. This can be used on all platforms. + */ +template +class SI_ConvertW { + const char * m_pEncoding; + UConverter * m_pConverter; +protected: + SI_ConvertW() : m_pEncoding(NULL), m_pConverter(NULL) { } +public: + SI_ConvertW(bool a_bStoreIsUtf8) : m_pConverter(NULL) { + m_pEncoding = a_bStoreIsUtf8 ? "UTF-8" : NULL; + } + + /* copy and assignment */ + SI_ConvertW(const SI_ConvertW & rhs) { operator=(rhs); } + SI_ConvertW & operator=(const SI_ConvertW & rhs) { + m_pEncoding = rhs.m_pEncoding; + m_pConverter = NULL; + return *this; + } + ~SI_ConvertW() { if (m_pConverter) ucnv_close(m_pConverter); } + + /** Calculate the number of UChar required for converting the input + * from the storage format. The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData Data in storage format to be converted to UChar. + * @param a_uInputDataLen Length of storage format data in bytes. This + * must be the actual length of the data, including + * NULL byte if NULL terminated string is required. + * @return Number of UChar required by the string when + * converted. If there are embedded NULL bytes in the + * input data, only the string up and not including + * the NULL byte will be converted. + * @return -1 cast to size_t on a conversion error. + */ + size_t SizeFromStore( + const char * a_pInputData, + size_t a_uInputDataLen) + { + SI_ASSERT(a_uInputDataLen != (size_t) -1); + + UErrorCode nError; + + if (!m_pConverter) { + nError = U_ZERO_ERROR; + m_pConverter = ucnv_open(m_pEncoding, &nError); + if (U_FAILURE(nError)) { + return (size_t) -1; + } + } + + nError = U_ZERO_ERROR; + int32_t nLen = ucnv_toUChars(m_pConverter, NULL, 0, + a_pInputData, (int32_t) a_uInputDataLen, &nError); + if (U_FAILURE(nError) && nError != U_BUFFER_OVERFLOW_ERROR) { + return (size_t) -1; + } + + return (size_t) nLen; + } + + /** Convert the input string from the storage format to UChar. + * The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData Data in storage format to be converted to UChar. + * @param a_uInputDataLen Length of storage format data in bytes. This + * must be the actual length of the data, including + * NULL byte if NULL terminated string is required. + * @param a_pOutputData Pointer to the output buffer to received the + * converted data. + * @param a_uOutputDataSize Size of the output buffer in UChar. + * @return true if all of the input data was successfully + * converted. + */ + bool ConvertFromStore( + const char * a_pInputData, + size_t a_uInputDataLen, + UChar * a_pOutputData, + size_t a_uOutputDataSize) + { + UErrorCode nError; + + if (!m_pConverter) { + nError = U_ZERO_ERROR; + m_pConverter = ucnv_open(m_pEncoding, &nError); + if (U_FAILURE(nError)) { + return false; + } + } + + nError = U_ZERO_ERROR; + ucnv_toUChars(m_pConverter, + a_pOutputData, (int32_t) a_uOutputDataSize, + a_pInputData, (int32_t) a_uInputDataLen, &nError); + if (U_FAILURE(nError)) { + return false; + } + + return true; + } + + /** Calculate the number of char required by the storage format of this + * data. The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData NULL terminated string to calculate the number of + * bytes required to be converted to storage format. + * @return Number of bytes required by the string when + * converted to storage format. This size always + * includes space for the terminating NULL character. + * @return -1 cast to size_t on a conversion error. + */ + size_t SizeToStore( + const UChar * a_pInputData) + { + UErrorCode nError; + + if (!m_pConverter) { + nError = U_ZERO_ERROR; + m_pConverter = ucnv_open(m_pEncoding, &nError); + if (U_FAILURE(nError)) { + return (size_t) -1; + } + } + + nError = U_ZERO_ERROR; + int32_t nLen = ucnv_fromUChars(m_pConverter, NULL, 0, + a_pInputData, -1, &nError); + if (U_FAILURE(nError) && nError != U_BUFFER_OVERFLOW_ERROR) { + return (size_t) -1; + } + + return (size_t) nLen + 1; + } + + /** Convert the input string to the storage format of this data. + * The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData NULL terminated source string to convert. All of + * the data will be converted including the + * terminating NULL character. + * @param a_pOutputData Pointer to the buffer to receive the converted + * string. + * @param a_pOutputDataSize Size of the output buffer in char. + * @return true if all of the input data, including the + * terminating NULL character was successfully + * converted. + */ + bool ConvertToStore( + const UChar * a_pInputData, + char * a_pOutputData, + size_t a_uOutputDataSize) + { + UErrorCode nError; + + if (!m_pConverter) { + nError = U_ZERO_ERROR; + m_pConverter = ucnv_open(m_pEncoding, &nError); + if (U_FAILURE(nError)) { + return false; + } + } + + nError = U_ZERO_ERROR; + ucnv_fromUChars(m_pConverter, + a_pOutputData, (int32_t) a_uOutputDataSize, + a_pInputData, -1, &nError); + if (U_FAILURE(nError)) { + return false; + } + + return true; + } +}; + +#endif // SI_CONVERT_ICU + + +// --------------------------------------------------------------------------- +// SI_CONVERT_WIN32 +// --------------------------------------------------------------------------- +#ifdef SI_CONVERT_WIN32 + +#define SI_Case SI_GenericCase + +// Windows CE doesn't have errno or MBCS libraries +#ifdef _WIN32_WCE +# ifndef SI_NO_MBCS +# define SI_NO_MBCS +# endif +#endif + +#include +#ifdef SI_NO_MBCS +# define SI_NoCase SI_GenericNoCase +#else // !SI_NO_MBCS +/** + * Case-insensitive comparison class using Win32 MBCS functions. This class + * returns a case-insensitive semi-collation order for MBCS text. It may not + * be safe for UTF-8 text returned in char format as we don't know what + * characters will be folded by the function! Therefore, if you are using + * SI_CHAR == char and SetUnicode(true), then you need to use the generic + * SI_NoCase class instead. + */ +#include +template +struct SI_NoCase { + bool operator()(const SI_CHAR * pLeft, const SI_CHAR * pRight) const { + if (sizeof(SI_CHAR) == sizeof(char)) { + return _mbsicmp((const unsigned char *)pLeft, + (const unsigned char *)pRight) < 0; + } + if (sizeof(SI_CHAR) == sizeof(wchar_t)) { + return _wcsicmp((const wchar_t *)pLeft, + (const wchar_t *)pRight) < 0; + } + return SI_GenericNoCase()(pLeft, pRight); + } +}; +#endif // SI_NO_MBCS + +/** + * Converts MBCS and UTF-8 to a wchar_t (or equivalent) on Windows. This uses + * only the Win32 functions and doesn't require the external Unicode UTF-8 + * conversion library. It will not work on Windows 95 without using Microsoft + * Layer for Unicode in your application. + */ +template +class SI_ConvertW { + UINT m_uCodePage; +protected: + SI_ConvertW() { } +public: + SI_ConvertW(bool a_bStoreIsUtf8) { + m_uCodePage = a_bStoreIsUtf8 ? CP_UTF8 : CP_ACP; + } + + /* copy and assignment */ + SI_ConvertW(const SI_ConvertW & rhs) { operator=(rhs); } + SI_ConvertW & operator=(const SI_ConvertW & rhs) { + m_uCodePage = rhs.m_uCodePage; + return *this; + } + + /** Calculate the number of SI_CHAR required for converting the input + * from the storage format. The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData Data in storage format to be converted to SI_CHAR. + * @param a_uInputDataLen Length of storage format data in bytes. This + * must be the actual length of the data, including + * NULL byte if NULL terminated string is required. + * @return Number of SI_CHAR required by the string when + * converted. If there are embedded NULL bytes in the + * input data, only the string up and not including + * the NULL byte will be converted. + * @return -1 cast to size_t on a conversion error. + */ + size_t SizeFromStore( + const char * a_pInputData, + size_t a_uInputDataLen) + { + SI_ASSERT(a_uInputDataLen != (size_t) -1); + + int retval = MultiByteToWideChar( + m_uCodePage, 0, + a_pInputData, (int) a_uInputDataLen, + 0, 0); + return (size_t)(retval > 0 ? retval : -1); + } + + /** Convert the input string from the storage format to SI_CHAR. + * The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData Data in storage format to be converted to SI_CHAR. + * @param a_uInputDataLen Length of storage format data in bytes. This + * must be the actual length of the data, including + * NULL byte if NULL terminated string is required. + * @param a_pOutputData Pointer to the output buffer to received the + * converted data. + * @param a_uOutputDataSize Size of the output buffer in SI_CHAR. + * @return true if all of the input data was successfully + * converted. + */ + bool ConvertFromStore( + const char * a_pInputData, + size_t a_uInputDataLen, + SI_CHAR * a_pOutputData, + size_t a_uOutputDataSize) + { + int nSize = MultiByteToWideChar( + m_uCodePage, 0, + a_pInputData, (int) a_uInputDataLen, + (wchar_t *) a_pOutputData, (int) a_uOutputDataSize); + return (nSize > 0); + } + + /** Calculate the number of char required by the storage format of this + * data. The storage format is always UTF-8. + * + * @param a_pInputData NULL terminated string to calculate the number of + * bytes required to be converted to storage format. + * @return Number of bytes required by the string when + * converted to storage format. This size always + * includes space for the terminating NULL character. + * @return -1 cast to size_t on a conversion error. + */ + size_t SizeToStore( + const SI_CHAR * a_pInputData) + { + int retval = WideCharToMultiByte( + m_uCodePage, 0, + (const wchar_t *) a_pInputData, -1, + 0, 0, 0, 0); + return (size_t) (retval > 0 ? retval : -1); + } + + /** Convert the input string to the storage format of this data. + * The storage format is always UTF-8 or MBCS. + * + * @param a_pInputData NULL terminated source string to convert. All of + * the data will be converted including the + * terminating NULL character. + * @param a_pOutputData Pointer to the buffer to receive the converted + * string. + * @param a_pOutputDataSize Size of the output buffer in char. + * @return true if all of the input data, including the + * terminating NULL character was successfully + * converted. + */ + bool ConvertToStore( + const SI_CHAR * a_pInputData, + char * a_pOutputData, + size_t a_uOutputDataSize) + { + int retval = WideCharToMultiByte( + m_uCodePage, 0, + (const wchar_t *) a_pInputData, -1, + a_pOutputData, (int) a_uOutputDataSize, 0, 0); + return retval > 0; + } +}; + +#endif // SI_CONVERT_WIN32 + + + +// --------------------------------------------------------------------------- +// SI_NO_CONVERSION +// --------------------------------------------------------------------------- +#ifdef SI_NO_CONVERSION + +#define SI_Case SI_GenericCase +#define SI_NoCase SI_GenericNoCase + +#endif // SI_NO_CONVERSION + + + +// --------------------------------------------------------------------------- +// TYPE DEFINITIONS +// --------------------------------------------------------------------------- + +typedef CSimpleIniTempl,SI_ConvertA > CSimpleIniA; +typedef CSimpleIniTempl,SI_ConvertA > CSimpleIniCaseA; + +#if defined(SI_NO_CONVERSION) +// if there is no wide char conversion then we don't need to define the +// widechar "W" versions of CSimpleIni +# define CSimpleIni CSimpleIniA +# define CSimpleIniCase CSimpleIniCaseA +# define SI_NEWLINE SI_NEWLINE_A +#else +# if defined(SI_CONVERT_ICU) +typedef CSimpleIniTempl,SI_ConvertW > CSimpleIniW; +typedef CSimpleIniTempl,SI_ConvertW > CSimpleIniCaseW; +# else +typedef CSimpleIniTempl,SI_ConvertW > CSimpleIniW; +typedef CSimpleIniTempl,SI_ConvertW > CSimpleIniCaseW; +# endif + +# ifdef _UNICODE +# define CSimpleIni CSimpleIniW +# define CSimpleIniCase CSimpleIniCaseW +# define SI_NEWLINE SI_NEWLINE_W +# else // !_UNICODE +# define CSimpleIni CSimpleIniA +# define CSimpleIniCase CSimpleIniCaseA +# define SI_NEWLINE SI_NEWLINE_A +# endif // _UNICODE +#endif + +#ifdef _MSC_VER +# pragma warning (pop) +#endif + +#endif // INCLUDED_SimpleIni_h + diff --git a/WebView2/WebView2.cpp b/WebView2/WebView2.cpp index 6eb16b6..360d06d 100644 --- a/WebView2/WebView2.cpp +++ b/WebView2/WebView2.cpp @@ -2,333 +2,1270 @@ #include "Plugin.h" #include "HostObjectRmAPI.h" #include "../API/RainmeterAPI.h" +#include + +inline bool ParseBool(const wchar_t* value) +{ + if (!value) return false; + + return _wcsicmp(value, L"true") == 0 || + _wcsicmp(value, L"1") == 0 || + _wcsicmp(value, L"yes") == 0 || + _wcsicmp(value, L"on") == 0; +} + +inline bool GetIniBool(CSimpleIniW& ini, bool& dirty, const wchar_t* section, const wchar_t* key, bool def) +{ + const wchar_t* value = ini.GetValue(section, key, nullptr); + if (!value) + { + ini.SetValue(section, key, def ? L"true" : L"false"); + dirty = true; + return def; + } + return ParseBool(value); +} + +inline std::wstring GetIniString(CSimpleIniW& ini, bool& dirty, const wchar_t* section, const wchar_t* key, const wchar_t* def) +{ + const wchar_t* value = ini.GetValue(section, key, nullptr); + if (!value) + { + ini.SetValue(section, key, def); + dirty = true; + return def; + } + return value; +} // 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; + + // Load or create config.ini + measure->ini.SetUnicode(); + measure->ini.LoadFile(measure->configPath.c_str()); + measure->iniDirty = false; + + // Read environment options from config.ini + bool extensions = GetIniBool(measure->ini, measure->iniDirty, L"Environment", L"Extensions", false); // Extensions + bool fluentBars = GetIniBool(measure->ini, measure->iniDirty, L"Environment", L"FluentOverlayScrollBars", true); // Fluent Bars + bool trackingPrevention = GetIniBool(measure->ini, measure->iniDirty, L"Environment", L"TrackingPrevention", true); // Tracking Prevention (SmartScreen) + std::wstring language = GetIniString(measure->ini, measure->iniDirty, L"Environment", L"BrowserLocale", L"system"); // Language + // Available browser flags: https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/webview-features-flags?tabs=win32cpp#available-webview2-browser-flags + std::wstring userBrowserArgs = GetIniString(measure->ini, measure->iniDirty, L"Environment", L"BrowserArguments", L"--allow-file-access-from-files"); // Browser Flags + std::wstring browserArgs; + browserArgs.append(L"--enable-features="); // Enable file access from file URLs + browserArgs.append(userBrowserArgs); + + // Add environment options + auto environmentOptions = Microsoft::WRL::Make(); + + environmentOptions->put_AdditionalBrowserArguments(browserArgs.c_str()); // Flags + + if (language == L"system") + environmentOptions->put_Language(measure->osLocale); // Browser Locale : System Locale + else + environmentOptions->put_Language(language.c_str()); // Browser Locale : Custom Locale + + + Microsoft::WRL::ComPtr environmentOptions5; + if (environmentOptions.As(&environmentOptions5) == S_OK) + { + environmentOptions5->put_EnableTrackingPrevention(trackingPrevention); // Tracking Prevention + } + Microsoft::WRL::ComPtr environmentOptions6; + if (environmentOptions.As(&environmentOptions6) == S_OK) + { + environmentOptions6->put_AreBrowserExtensionsEnabled(extensions); // Extensions + } + Microsoft::WRL::ComPtr environmentOptions8; + if (environmentOptions.As(&environmentOptions8) == S_OK) + { + environmentOptions8->put_ScrollBarStyle(fluentBars ? COREWEBVIEW2_SCROLLBAR_STYLE_FLUENT_OVERLAY : COREWEBVIEW2_SCROLLBAR_STYLE_DEFAULT); // Set Fluent Scrollbars + } + + // Create WebView2 environment with user data folder + HRESULT hr = CreateCoreWebView2EnvironmentWithOptions( + nullptr, measure->userDataFolder.c_str(), environmentOptions.Get(), + Callback( + measure, + &Measure::CreateEnvironmentHandler + ).Get() + + ); + + if (!SUCCEEDED(hr)) + { + if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) + { + int result = MessageBox( + measure->skinWindow, + L"Failed to start creation process.\n\nMake sure WebView2 Runtime is installed.\n\n" + L"Would you like to be redirected to the WebView2 Runtime download page?", + L"Missing Dependency", + MB_ICONERROR | MB_YESNO + ); + + if (result == IDYES) + { + ShellExecuteW( + nullptr, + L"open", + L"https://developer.microsoft.com/microsoft-edge/webview2/", + nullptr, + nullptr, + SW_SHOWNORMAL + ); + } + measure->FailWebView(hr, L"WebView2: Failed to start environment creation process. Make sure WebView2 Runtime is installed."); + } + else + { + ShowFailure(hr, L"Failed to create webview environment."); + measure->FailWebView(hr, L"Failed to create webview environment."); + } + } } // 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 (!SUCCEEDED(result)) + { + return FailWebView(result, L"WebView2: Failed to create webview environment."); + } + + webViewEnvironment = env; + + + // Create WebView2 controller with options. + auto webViewEnvironment10 = webViewEnvironment.try_query(); + if (!webViewEnvironment10) + { + FeatureNotAvailable(); + + webViewEnvironment->CreateCoreWebView2Controller( + skinWindow, + Callback( + this, + &Measure::CreateControllerHandler + ).Get() + ); + } + else + { + wil::com_ptr controllerOptions; + HRESULT hr = webViewEnvironment10->CreateCoreWebView2ControllerOptions(&controllerOptions); + if (hr == E_INVALIDARG) + { + ShowFailure(hr, L"Unable to create WebView2 due to an invalid profile name."); + return S_OK; + } + CHECK_FAILURE(hr); + + // Read environment options from config.ini + std::wstring scriptLocale = GetIniString(ini, iniDirty, L"Controller", L"ScriptLocale", L"system"); + bool privateMode = GetIniBool(ini, iniDirty, L"Controller", L"PrivateMode", false); + + // OPTIONS + controllerOptions->put_ProfileName(L"rainmeter"); // Profile Name + controllerOptions->put_IsInPrivateModeEnabled(privateMode); // Private/Incognito Mode + if (SUCCEEDED(controllerOptions->QueryInterface( + IID_PPV_ARGS(&webViewControllerOptions2)))) + { + + if (scriptLocale == L"system") + { + webViewControllerOptions2->put_ScriptLocale(osLocale); // System Locale + } + else + { + webViewControllerOptions2->put_ScriptLocale(scriptLocale.c_str()); // Custom Locale + } + } + + // Set Transparent Background + wil::com_ptr controllerOptions3; + if (SUCCEEDED(controllerOptions->QueryInterface(IID_PPV_ARGS(&controllerOptions3)))) + { + COREWEBVIEW2_COLOR transparentColor = { 0, 0, 0, 0 }; + controllerOptions3->put_DefaultBackgroundColor(transparentColor); // Background Color + } + + // Create Controller With Options. + webViewEnvironment10->CreateCoreWebView2ControllerWithOptions( + skinWindow, + controllerOptions.get(), + Callback( + this, + &Measure::CreateControllerHandler + ).Get() + ); + } + + return S_OK; +} + +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); +} + +void RegisterFrames(Measure* measure, ICoreWebView2Frame* rawFrame, int level) +{ + wil::com_ptr frame = rawFrame; + + wil::com_ptr frame2 = frame.try_query(); + wil::com_ptr frame5 = frame.try_query(); + + // 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 + + CHECK_FAILURE(frame2->AddHostObjectToScriptWithOrigins(L"RainmeterAPI", &hostObjectVariant, 1, &origins)); + VariantClear(&hostObjectVariant); + + auto newFrameState = std::make_shared(); + newFrameState->frame = frame2; + newFrameState->injected = false; + newFrameState->isDestroyed = false; + + measure->webViewFrames.push_back(newFrameState); + + Frames* frameState = newFrameState.get(); + + // Inject frame ancestor to nested frames to allow framing websites. (Requires virtual host or http-server). + frame2->add_NavigationStarting( + Microsoft::WRL::Callback( + [measure, origin, frameState](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 + ); + + frame2->add_ContentLoading( + Callback( + [measure, frameState](ICoreWebView2Frame* sender, ICoreWebView2ContentLoadingEventArgs* args) -> HRESULT + { + return S_OK; + } + ).Get(), nullptr + ); + + frame2->add_DOMContentLoaded( + Callback( + [measure, frameState](ICoreWebView2Frame* sender, ICoreWebView2DOMContentLoadedEventArgs* args) -> HRESULT + { + return S_OK; + } + ).Get(), nullptr + ); + + frame2->add_NavigationCompleted( + Callback( + [measure, frameState](ICoreWebView2Frame* sender, ICoreWebView2NavigationCompletedEventArgs* args) -> HRESULT + { + return S_OK; + } + ).Get(), nullptr + ); + + frame2->add_Destroyed( + Callback( + [measure, level, frameState](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()); + } + + frameState->isDestroyed = true; + + return S_OK; + } + ).Get(), nullptr + ); + + wil::com_ptr frame7 = frame.try_query(); + + if (frame7) + { + CHECK_FAILURE(frame7->add_FrameCreated( + Microsoft::WRL::Callback( + [measure, level](ICoreWebView2Frame* sender, ICoreWebView2FrameCreatedEventArgs* args) -> HRESULT + { + wil::com_ptr childFrame; + CHECK_FAILURE(args->get_Frame(&childFrame)); + // RECURSIVE CALL: + RegisterFrames(measure, childFrame.get(), level + 1); + return S_OK; + }).Get(), nullptr)); + } } // 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"); + } + + if (result == S_OK) + { + // WebView is initializing + SetStateAndNotify(0); + + webViewController = controller; + CHECK_FAILURE(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; + } + + // CONTROLLER OPTIONS + webViewController->put_Bounds(bounds); // Set initial bounds + webViewController->put_IsVisible(visible); // Set initial visibility + webViewController->put_ZoomFactor(zoomFactor); // Set initial zoom factor + + // Set Focus when required. + webViewController->add_MoveFocusRequested( + Microsoft::WRL::Callback( + [this](ICoreWebView2Controller* sender, ICoreWebView2MoveFocusRequestedEventArgs* args) -> HRESULT + { + enum COREWEBVIEW2_MOVE_FOCUS_REASON reason; + args->get_Reason(&reason); + + if (reason == COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC) + args->put_Handled(TRUE); + + return S_OK; + } + ).Get(), nullptr + ); + + // Accelerator Keys + webViewController->add_AcceleratorKeyPressed( + Microsoft::WRL::Callback( + [this](ICoreWebView2Controller* sender, ICoreWebView2AcceleratorKeyPressedEventArgs* args) -> HRESULT + { + COREWEBVIEW2_KEY_EVENT_KIND kind; + args->get_KeyEventKind(&kind); + // We only care about key down events. + if (kind == COREWEBVIEW2_KEY_EVENT_KIND_KEY_DOWN || + kind == COREWEBVIEW2_KEY_EVENT_KIND_SYSTEM_KEY_DOWN) + { + UINT key; + args->get_VirtualKey(&key); + + wil::com_ptr args2; + args->QueryInterface(IID_PPV_ARGS(&args2)); + + if (args2) + { + if (!assistiveFeatures) // Disable Assistive Features Shortcut Keys + { + if (key == 'F') + { + args2->put_IsBrowserAcceleratorKeyEnabled(FALSE); + } + if (key == 'G') + { + args2->put_IsBrowserAcceleratorKeyEnabled(FALSE); + } + if (key == 'G' && (GetKeyState(VK_CONTROL) < 0) && (GetKeyState(VK_SHIFT) < 0)) + { + args2->put_IsBrowserAcceleratorKeyEnabled(FALSE); + } + if (key == 'P') + { + args2->put_IsBrowserAcceleratorKeyEnabled(FALSE); + } + if (key == 'P' && (GetKeyState(VK_CONTROL) < 0) && (GetKeyState(VK_SHIFT) < 0)) + { + args2->put_IsBrowserAcceleratorKeyEnabled(FALSE); + } + if (key == VK_F3) + { + args2->put_IsBrowserAcceleratorKeyEnabled(FALSE); + } + if (key == VK_F7) + { + args2->put_IsBrowserAcceleratorKeyEnabled(FALSE); + } + } + } + } + return S_OK; + } + ).Get(), nullptr + ); + + // CORE SETTINGS + CHECK_FAILURE(webView->get_Settings(&webViewSettings)); + webViewSettings->put_IsScriptEnabled(TRUE); + webViewSettings->put_AreDefaultScriptDialogsEnabled(TRUE); + webViewSettings->put_IsWebMessageEnabled(TRUE); + webViewSettings->put_AreHostObjectsAllowed(TRUE); + webViewSettings->put_AreDevToolsEnabled(TRUE); + webViewSettings->put_IsZoomControlEnabled(zoomControl); + + // Read environment options from config.ini + bool statusBar = GetIniBool(ini, iniDirty, L"Core", L"StatusBar", true); + bool pinchZoom = GetIniBool(ini, iniDirty, L"Core", L"PinchZoom", true); + bool swipeNavigation = GetIniBool(ini, iniDirty, L"Core", L"SwipeNavigation", true); + bool reputationChecking = GetIniBool(ini, iniDirty, L"Core", L"SmartScreen", true); + + webViewSettings->put_IsStatusBarEnabled(statusBar); + + webViewSettings2 = webViewSettings.try_query(); + if (webViewSettings2 && !userAgent.empty()) + { + webViewSettings2->put_UserAgent(userAgent.c_str()); // User Agent + } + + auto settings5 = webViewSettings.try_query(); + if (settings5) + { + settings5->put_IsPinchZoomEnabled(pinchZoom); // Pinch Zoom + } + + auto settings6 = webViewSettings.try_query(); + if (settings6) + { + settings6->put_IsSwipeNavigationEnabled(swipeNavigation); // Swipe Navigation + } + + auto settings7 = webViewSettings.try_query(); + if (settings7) + { + settings7->put_HiddenPdfToolbarItems(COREWEBVIEW2_PDF_TOOLBAR_ITEMS_FULL_SCREEN); // Hide pdf reader's full-screen button + } + + auto settings8 = webViewSettings.try_query(); + if (settings8) + { + settings8->put_IsReputationCheckingRequired(reputationChecking); // SmartScreen reputation checking + } + + auto settings9 = webViewSettings.try_query(); + if (settings9) + { + settings9->put_IsNonClientRegionSupportEnabled(TRUE); // Enable app-region css style support + } + + // PROFILE OPTIONS + auto webView2_13 = webView.try_query(); + if (webView2_13) + { + wil::com_ptr profile; + CHECK_FAILURE(webView2_13->get_Profile(&profile)); + + // Read environment options from config.ini + std::wstring downloadsFolder = GetIniString(ini, iniDirty, L"Profile", L"DownloadsFolderPath", L""); + std::wstring colorScheme = GetIniString(ini, iniDirty, L"Profile", L"ColorScheme", L"system"); + bool passAutoSave = GetIniBool(ini, iniDirty, L"Profile", L"PasswordAutoSave", false); + bool generalAutoFill = GetIniBool(ini, iniDirty, L"Profile", L"GeneralAutoFill", true); + + profile->put_DefaultDownloadFolderPath(downloadsFolder.c_str()); // Downloads folder path + + if (_wcsicmp(colorScheme.c_str(), L"light") == 0) // Color Scheme + { + profile->put_PreferredColorScheme(COREWEBVIEW2_PREFERRED_COLOR_SCHEME_LIGHT); + } + else if (_wcsicmp(colorScheme.c_str(), L"dark") == 0) + { + profile->put_PreferredColorScheme(COREWEBVIEW2_PREFERRED_COLOR_SCHEME_DARK); + } + else + { + profile->put_PreferredColorScheme(COREWEBVIEW2_PREFERRED_COLOR_SCHEME_AUTO); + } + + auto profile6 = webViewSettings.try_query(); + if (profile6) + { + profile6->put_IsPasswordAutosaveEnabled(passAutoSave); // Password AutoSave + profile6->put_IsGeneralAutofillEnabled(generalAutoFill); // General AutoFill + } + + auto profile7 = webViewSettings.try_query(); + if (profile7) + { + webViewProfile7 = profile7; // For browser extensions (TODO) + } + } + + // 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; + CHECK_FAILURE(webView->AddHostObjectToScript(L"RainmeterAPI", &variant)); + VariantClear(&variant); + + // Add script to make RainmeterAPI available globally + webView->AddScriptToExecuteOnDocumentCreated( + L"window.RainmeterAPI = chrome.webview.hostObjects.sync.RainmeterAPI;", + nullptr + ); + + webView3 = webView.try_query(); + if (webView3) + { + if (!hostPath.empty() && webView3) + { + webView3->SetVirtualHostNameToFolderMapping( + hostName.c_str(), hostPath.c_str(), COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_ALLOW); + } + } + // Frames + wil::com_ptr webView4 = webView.try_query(); + if (webView4) { + webView4->add_FrameCreated( + Microsoft::WRL::Callback( + [this, variant](ICoreWebView2* sender, ICoreWebView2FrameCreatedEventArgs* args) -> HRESULT + { + wil::com_ptr frame; + args->get_Frame(&frame); + + BOOL isDestroyed; + frame->IsDestroyed(&isDestroyed); + + if (isDestroyed) return S_OK; + + RegisterFrames(this, frame.get(), 1); + + return S_OK; + } + ).Get(), 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 + ); + + // For Task Manager + webView6 = webView.try_query(); + + // Skin Context Menu + wil::com_ptr webView11 = webView.try_query(); + if (webView11) { + webView11->add_ContextMenuRequested( + Microsoft::WRL::Callback( + [this](ICoreWebView2* sender, ICoreWebView2ContextMenuRequestedEventArgs* args) -> HRESULT + { + // Show browser's context menu + wil::com_ptr items; + args->get_MenuItems(&items); + wil::com_ptr target; + args->get_ContextMenuTarget(&target); + COREWEBVIEW2_CONTEXT_MENU_TARGET_KIND context_kind; + target->get_Kind(&context_kind); + UINT32 itemsCount; + items->get_Count(&itemsCount); + + wil::unique_cotaskmem_string documentTile; + webView->get_DocumentTitle(&documentTile); + + bool isViewSource = documentTile && wcsncmp(documentTile.get(), L"view-source:", 12) == 0; + + // Add Task Manager, Downloads and View Page Source items + wil::com_ptr webviewEnvironment9 = webViewEnvironment.try_query(); + if (webviewEnvironment9 && webView6) + { + + wil::com_ptr taskManagerItem; + webviewEnvironment9->CreateContextMenuItem(L"Task manager", nullptr, COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_COMMAND, &taskManagerItem); + + taskManagerItem->add_CustomItemSelected( // Task Manager + Callback( + [this]( + ICoreWebView2ContextMenuItem* sender, + IUnknown* args) + { + webView6->OpenTaskManagerWindow(); + return S_OK; + }) + .Get(), + nullptr); + + wil::com_ptr viewPageSource; + + if (!isViewSource) + { + webviewEnvironment9->CreateContextMenuItem(L"View page source", nullptr, COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_COMMAND, &viewPageSource); + + viewPageSource->add_CustomItemSelected( // View Page Source + Callback( + [this, target]( + ICoreWebView2ContextMenuItem* sender, + IUnknown* args) + { + INPUT inputs[4] = {}; + + inputs[0].type = INPUT_KEYBOARD; + inputs[0].ki.wVk = VK_CONTROL; + + inputs[1].type = INPUT_KEYBOARD; + inputs[1].ki.wVk = 'U'; + + inputs[2].type = INPUT_KEYBOARD; + inputs[2].ki.wVk = 'U'; + inputs[2].ki.dwFlags = KEYEVENTF_KEYUP; + + inputs[3].type = INPUT_KEYBOARD; + inputs[3].ki.wVk = VK_CONTROL; + inputs[3].ki.dwFlags = KEYEVENTF_KEYUP; + + SendInput(4, inputs, sizeof(INPUT)); + return S_OK; + }) + .Get(), + nullptr); + } + else + { + webviewEnvironment9->CreateContextMenuItem(L"Exit View Source", nullptr, COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_COMMAND, &viewPageSource); + viewPageSource->add_CustomItemSelected( // View Page Source + Callback( + [this]( + ICoreWebView2ContextMenuItem* sender, + IUnknown* args) + { + wil::unique_cotaskmem_string currentUrl; + webView->get_Source(¤tUrl); + webView->Navigate(currentUrl.get()); + return S_OK; + }) + .Get(), + nullptr); + } + + wil::com_ptr downloads; + webviewEnvironment9->CreateContextMenuItem(L"Downloads", nullptr, COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_COMMAND, &downloads); + + downloads->add_CustomItemSelected( // Downloads + Callback( + [this, target]( + ICoreWebView2ContextMenuItem* sender, + IUnknown* args) + { + INPUT inputs[4] = {}; + + inputs[0].type = INPUT_KEYBOARD; + inputs[0].ki.wVk = VK_CONTROL; + + inputs[1].type = INPUT_KEYBOARD; + inputs[1].ki.wVk = 'J'; + + inputs[2].type = INPUT_KEYBOARD; + inputs[2].ki.wVk = 'J'; + inputs[2].ki.dwFlags = KEYEVENTF_KEYUP; + + inputs[3].type = INPUT_KEYBOARD; + inputs[3].ki.wVk = VK_CONTROL; + inputs[3].ki.dwFlags = KEYEVENTF_KEYUP; + + SendInput(4, inputs, sizeof(INPUT)); + return S_OK; + }) + .Get(), + nullptr); + + wil::com_ptr separator; + webviewEnvironment9->CreateContextMenuItem(L"View page source", nullptr, COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_SEPARATOR, &separator); + + wil::com_ptr skinMenu; + webviewEnvironment9->CreateContextMenuItem(L"Skin menu", nullptr, COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_COMMAND, &skinMenu); + + skinMenu->add_CustomItemSelected( // Skin Menu + Callback( + [this, target]( + ICoreWebView2ContextMenuItem* sender, + IUnknown* args) + { + RmExecute(skin, L"[!SkinMenu]"); + return S_OK; + }) + .Get(), + nullptr); + + items->InsertValueAtIndex(itemsCount, skinMenu.get()); + items->InsertValueAtIndex(itemsCount, separator.get()); + items->InsertValueAtIndex(itemsCount, taskManagerItem.get()); + items->InsertValueAtIndex(itemsCount, viewPageSource.get()); + items->InsertValueAtIndex(itemsCount - 1, downloads.get()); + items->InsertValueAtIndex(itemsCount, separator.get()); + } + + if (!newWindow) + { + // Remove Open link in new window item + wil::com_ptr current; + for (UINT32 i = 0; i < itemsCount; i++) + { + items->GetValueAtIndex(i, ¤t); + wil::unique_cotaskmem_string name; + current->get_Name(&name); + + if (wcscmp(name.get(), L"openLinkInNewWindow") == 0) + { + items->RemoveValueAtIndex(i); + i--; + itemsCount--; + break; + } + } + } + + if (!assistiveFeatures) + { + // Remove Find and Print items + wil::com_ptr current; + for (UINT32 i = 0; i < itemsCount; i++) + { + items->GetValueAtIndex(i, ¤t); + wil::unique_cotaskmem_string name; + current->get_Name(&name); + if (wcscmp(name.get(), L"find") == 0 || + wcscmp(name.get(), L"print") == 0) + { + items->RemoveValueAtIndex(i); + i--; + itemsCount--; + } + } + } + + 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; + } + + wil::unique_cotaskmem_string uri; + args->get_Uri(&uri); + + bool isViewSource = uri && wcsncmp(uri.get(), L"view-source:", 12) == 0; + std::wstring viewSourceUrl = uri.get() + 12; + bool isVirtualHost = !viewSourceUrl.empty() && viewSourceUrl.find(hostName.c_str()) != std::wstring::npos; + + // if url is view-source and contains hostname, open in same window: + if (isViewSource && isVirtualHost) + { + sender->Navigate(uri.get()); + args->put_Handled(TRUE); + return S_OK; + } + else if (isViewSource) // if url is view-source only, open in new window: + { + args->put_Handled(FALSE); + return S_OK; + } + + if (newWindow) + { + // Open in new window + args->put_Handled(FALSE); + } + else + { + // Open in same window + sender->Navigate(uri.get()); + args->put_Handled(TRUE); + } + + 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 (notifications) // 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 + { + wil::unique_cotaskmem_string initialUri; + sender->get_Source(&initialUri); + wil::unique_cotaskmem_string destinationUri; + args->get_Uri(&destinationUri); + + // Determine if this is the first load or a reload + if (initialUri && destinationUri && + wcscmp(initialUri.get(), destinationUri.get()) == 0) // if same URL, it's a reload + { + isFirstLoad = false; + } + else // different URL, it's a first load + { + isFirstLoad = true; + } + + // 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) + { + currentUrl = newUrl; + if (wcslen(onUrlChangeAction.c_str()) > 0) + { + if (skin) RmExecute(skin, onUrlChangeAction.c_str()); + } + } + } + + return S_OK; + } + ).Get(), nullptr + ); + + webView->add_DocumentTitleChanged( + Callback( + [this](ICoreWebView2* sender, IUnknown* args) -> HRESULT + { + + // Read current Url + wil::unique_cotaskmem_string uri; + sender->get_Source(&uri); + + // Read document title. + wil::unique_cotaskmem_string documentTile; + sender->get_DocumentTitle(&documentTile); + currentTitle = documentTile.get(); + + bool isViewSource = documentTile && wcsncmp(documentTile.get(), L"view-source:", 12) == 0; + // Look for view-source on document title: + if (isViewSource) + { + currentUrl = documentTile.get(); + + if (wcslen(onUrlChangeAction.c_str()) > 0) + { + if (skin) RmExecute(skin, onUrlChangeAction.c_str()); + } + } + else + { + // Look for view-source on current url and remove it: + if (wcsncmp(currentUrl.c_str(), L"view-source:", 12) == 0) + { + currentUrl = uri.get(); + if (wcslen(onUrlChangeAction.c_str()) > 0) + { + if (skin) RmExecute(skin, onUrlChangeAction.c_str()); + } + } + } + + + return S_OK; + } + ).Get(), nullptr + ); + + // Add ContentLoading event to call action when page starts loading + webView->add_ContentLoading( + Callback( + [this](ICoreWebView2* sender, ICoreWebView2ContentLoadingEventArgs* args) -> HRESULT + { + // 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 call action when DOM is loaded + wil::com_ptr webView2 = webView.try_query(); + if (webView2) { + + webView2->add_DOMContentLoaded( + Callback< ICoreWebView2DOMContentLoadedEventHandler>( + [this](ICoreWebView2* sender, ICoreWebView2DOMContentLoadedEventArgs* args) -> HRESULT + { + // DOM content is loaded + SetStateAndNotify(300); + + 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 + { + // 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 + { + return S_OK; + } + ).Get() + ); + + if (isFirstLoad) // First load + { + if (wcslen(onPageFirstLoadAction.c_str()) > 0) + { + if (skin) + RmExecute(skin, onPageFirstLoadAction.c_str()); + } + isFirstLoad = false; + } + else // Page reload + { + if (wcslen(onPageReloadAction.c_str()) > 0) + { + if (skin) + RmExecute(skin, onPageReloadAction.c_str()); + } + } + // Common action after any page load + if (wcslen(onPageLoadFinishAction.c_str()) > 0) + { + if (skin) + RmExecute(skin, onPageLoadFinishAction.c_str()); + } + return S_OK; + } + ).Get(), nullptr + ); + + if (iniDirty) + { + ini.SaveFile(configPath.c_str()); + iniDirty = false; + } + + initialized = true; + + //if (rm) RmLog(rm, LOG_DEBUG, 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 SkinControl state + UpdateChildWindowState(this, ((clickthrough == 1 || clickthrough >= 3) ? false : true)); + } + return S_OK; +} + +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; +} + +void RestartWebView2(Measure* measure) +{ + if (!measure || !measure->skinWindow) + return; - 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); - - // 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 - ); - - // Add NavigationStarting event to call action when navigation starts - webView->add_NavigationStarting( - Callback( - [this](ICoreWebView2* sender, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT - { - if (wcslen(onPageLoadStartAction.c_str()) > 0) - { - if (skin) - RmExecute(skin, onPageLoadStartAction.c_str()); - } - return S_OK; - } - ).Get(), - nullptr - ); - - // Add ContentLoading event to call action when page starts loading - webView->add_ContentLoading( - Callback( - [this](ICoreWebView2* sender, ICoreWebView2ContentLoadingEventArgs* args) -> HRESULT - { - if (wcslen(onPageLoadingAction.c_str()) > 0) - { - if (skin) - RmExecute(skin, onPageLoadingAction.c_str()); - } - return S_OK; - } - ).Get(), - nullptr - ); - - // Add NavigationCompleted event to call OnInitialize after page loads and handle load actions - webView->add_NavigationCompleted( - Callback( - [this](ICoreWebView2* sender, ICoreWebView2NavigationCompletedEventArgs* args) -> HRESULT - { - isAllowDualControlInjected = false; - - // Inject script to capture page load events for drag/move and context menu - if (allowDualControl) - { - InjectAllowDualControl(this); - } - - // Call JavaScript OnInitialize callback if it exists and capture return value - webView->ExecuteScript( - L"(function() { if (typeof window.OnInitialize === 'function') { var result = window.OnInitialize(); return result !== undefined ? String(result) : ''; } return ''; })();", - 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; - } - else // Page reload - { - if (wcslen(onPageReloadAction.c_str()) > 0) - { - if (skin) - RmExecute(skin, onPageReloadAction.c_str()); - } - } - // Common action after any page load - if (wcslen(onPageLoadFinishAction.c_str()) > 0) - { - if (skin) - RmExecute(skin, onPageLoadFinishAction.c_str()); - } - return S_OK; - } - ).Get(), - nullptr - ); - - // Navigate to URL - if (!url.empty()) - { - 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 (!measure->initialized) + { + RmLog(measure->rm, LOG_ERROR, L"WebView2: Not running"); + return; + } + + // Stop WebView2 + StopWebView2(measure); + + // Start WebView2 + if (!measure->isStopping) + { + CreateWebView2(measure); + } } + +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; + + measure->webViewFrames.clear(); + + // Stop navigation + if (measure->webView) + { + measure->webView->Stop(); + } + + // Close the Controller + if (measure->webViewController) + { + measure->webViewController->put_IsVisible(FALSE); + measure->webViewController->Close(); + } + + // Release + measure->webViewController.reset(); + measure->webView.reset(); + measure->webViewSettings.reset(); + measure->webViewSettings2.reset(); + measure->webViewEnvironment.reset(); + + // Reset flags + measure->initialized = false; + measure->isFirstLoad = true; + measure->isViewSource = false; + + // Clear url + measure->currentUrl.clear(); + + // WebView is stopped + measure->SetStateAndNotify(-1); + + if (wcslen(measure->onWebViewStopAction.c_str()) > 0) + { + if (measure->skin) + RmExecute(measure->skin, measure->onWebViewStopAction.c_str()); + } + + measure->isStopping = false; +} \ No newline at end of file diff --git a/WebView2/WebView2.rc b/WebView2/WebView2.rc index 9b0f1ed..a1ce756 100644 Binary files a/WebView2/WebView2.rc and b/WebView2/WebView2.rc differ diff --git a/WebView2/WebView2.vcxproj b/WebView2/WebView2.vcxproj index dbdb55c..222ef65 100644 --- a/WebView2/WebView2.vcxproj +++ b/WebView2/WebView2.vcxproj @@ -31,6 +31,7 @@ + @@ -201,8 +202,8 @@ - + Static @@ -211,7 +212,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..b52d63e 100644 --- a/WebView2/WebView2.vcxproj.filters +++ b/WebView2/WebView2.vcxproj.filters @@ -8,14 +8,12 @@ - - - + diff --git a/WebView2/packages.config b/WebView2/packages.config index 4662d01..50278c8 100644 --- a/WebView2/packages.config +++ b/WebView2/packages.config @@ -1,5 +1,5 @@ - + - + - + \ No newline at end of file