Project

General

Profile

Feature #13788 » ThemeToggleButton.cpp

Christian Meyer, 07/02/2025 12:09 AM

 
#include "ThemeToggleButton.hpp"

#include <Wt/WPushButton.h>
#include <Wt/WPopupMenu.h>
#include <Wt/WMenuItem.h>

#include <string>
#include <Wt/WString.h>

#include <Wt/WIcon.h>


namespace
{
Wt::WString autoIconString;
Wt::WString ItemStringAuto;

Wt::WString lightIconString;
Wt::WString ItemStringLight;

Wt::WString darkIconString;
Wt::WString ItemStringDark;

Wt::WString MenuButtonString;

void setupItemStrings()
{
auto tempIconString = Wt::WString::tr("ThemeToggle.Auto.Icon");
autoIconString =
tempIconString == "??ThemeToggle.Auto.Icon??" ?
Wt::WString("<i class=\"bi bi-circle-half\"></i> ")
: tempIconString;

auto tempItemString = Wt::WString::tr("ThemeToggle.Auto");

ItemStringAuto =
Wt::WString("{1}")
.arg(
tempItemString == "??ThemeToggle.Auto??" ?
"Auto"
: tempItemString
);
tempIconString = Wt::WString::tr("ThemeToggle.Light.Icon");
lightIconString =
tempIconString == "??ThemeToggle.Light.Icon??" ?
Wt::WString("<i class=\"bi bi-sun-fill\"></i> ")
: tempIconString;

tempItemString = Wt::WString::tr("ThemeToggle.Light");
ItemStringLight =
Wt::WString("{1}")
.arg(
tempItemString == "??ThemeToggle.Light??" ?
"Light"
: tempItemString
);
tempIconString = Wt::WString::tr("ThemeToggle.Dark.Icon");
darkIconString =
tempIconString == "??ThemeToggle.Dark.Icon??" ?
Wt::WString("<i class=\"bi bi-moon-stars-fill\"></i> ")
: tempIconString;
tempItemString = Wt::WString::tr("ThemeToggle.Dark");
ItemStringDark =
Wt::WString("{1}")
.arg(
tempItemString == "??ThemeToggle.Dark??" ?
"Dark"
: tempItemString
);
tempItemString = Wt::WString::tr("ThemeToggle.Menu");
MenuButtonString =
Wt::WString("{2}{1}")
.arg(
tempItemString == "??ThemeToggle.Menu??" ?
"Toggle theme"
: tempItemString
);
}

std::string jsPrepared = R"JSDelim(

/*!
* Based on:
*
* Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
* Copyright 2011-2025 The Bootstrap Authors
* Licensed under the Creative Commons Attribution 3.0 Unported License.
*
* Removed pure js setup, enabled cooperation with Wt
*/

window.getStoredTheme = function(){ return localStorage.getItem('theme'); }
window.setStoredTheme = function(theme){ localStorage.setItem('theme', theme); }

window.getPreferredTheme = function(){
const storedTheme = getStoredTheme()
if (storedTheme) {
return storedTheme
}

return window.matchMedia('(prefers-color-scheme: dark)')
.matches ? 'dark' : 'light'
}

window.setTheme = function(theme)
{
// if (theme === 'auto') { // pure BS 5.3 way
// document.documentElement.setAttribute(
// 'data-bs-theme',
// (
// window.matchMedia('(prefers-color-scheme: dark)')
// .matches ? 'dark' : 'light'
// )
// )
// } else {
// document.documentElement.setAttribute('data-bs-theme', theme)
// }

// Mixed Bootstrap 5.2.3 & 5.3 way
// Remove existing theme classes (if any, like 'light' or 'dark' from an older setup)
document.documentElement.classList.remove('light', 'dark');
document.body.classList.remove('light', 'dark'); // Often applied to body as well

if (theme === 'auto') {
const preferred = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
document.documentElement.setAttribute('data-bs-theme', preferred); // You can keep this for future 5.3+ upgrade
document.body.classList.add(preferred); // Add the class for 5.2.3
} else {
document.documentElement.setAttribute('data-bs-theme', theme); // Keep this for future 5.3+ upgrade
document.body.classList.add(theme); // Add the class for 5.2.3
}
}

// console.log("Theme functions defined."); // debug logs


// Initial theme setting
setTheme(getPreferredTheme());

// Media query listener
const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
mediaQueryList.addEventListener('change', (e) => {
const storedTheme = getStoredTheme();
if (storedTheme !== 'light' && storedTheme !== 'dark') {
setTheme(getPreferredTheme());
}
});

(() => {
// fire JSignal
{1} // gets replaced with JSignal call

// console.log("Theme Notified on Load"); // debug logs


})(); // run function on load

// console.log("Script execution finished (outside listener)."); // debug logs

)JSDelim";

const std::string slotJS = R"JSDelim(

function(obj, event, theme){
// console.log("Theme Selection:", theme); // debug logs
setStoredTheme(theme)
setTheme(theme)
}

)JSDelim";
}


namespace Bootstrap5
{

ThemeToggleButton::ThemeToggleButton()
: selected(this, "notifyTheme"), themeSelect(slotJS, 1, this)
{
setupItemStrings();
prepareContent();
setupConnections();
doJavaScript(Wt::WString(jsPrepared).arg(selected.createCall({"getPreferredTheme()"})).toUTF8());
}

void ThemeToggleButton::prepareContent()
{
setTextFormat(Wt::TextFormat::XHTML);
setText(Wt::WString(MenuButtonString).arg(autoIconString)); // sets Text with auto Icon
{
auto uPop = std::make_unique<Wt::WPopupMenu>();
popMenu = uPop.get();
setMenu(std::move(uPop));
}
{
auto uItem = std::make_unique<Wt::WMenuItem>(ItemStringLight);

auto iconText = std::make_unique<Wt::WText>(lightIconString);
iconText->setTextFormat(Wt::TextFormat::XHTML);
iconText->addStyleClass("me-2 opacity-50");
uItem->anchor()->insertWidget(0, std::move(iconText));

uItem->triggered().connect([this](){
themeSelect.exec(this->jsRef(), "null", "\""+ Theme::Light +"\"");
this->setText(Wt::WString(MenuButtonString).arg(lightIconString));
s_themeChanged(Theme::Light);
});

popMenu->addItem(std::move(uItem));
}
{
auto uItem = std::make_unique<Wt::WMenuItem>(ItemStringDark);
auto iconText = std::make_unique<Wt::WText>(darkIconString);
iconText->setTextFormat(Wt::TextFormat::XHTML);
iconText->addStyleClass("me-2 opacity-50");
uItem->anchor()->insertWidget(0, std::move(iconText));

uItem->triggered().connect([this](){
themeSelect.exec(this->jsRef(), "null", "\""+ Theme::Dark +"\"");
this->setText(Wt::WString(MenuButtonString).arg(darkIconString));
s_themeChanged(Theme::Dark);
});

popMenu->addItem(std::move(uItem));
}
{
auto uItem = std::make_unique<Wt::WMenuItem>(ItemStringAuto);

auto iconText = std::make_unique<Wt::WText>(autoIconString);
iconText->setTextFormat(Wt::TextFormat::XHTML);
iconText->addStyleClass("me-2 opacity-50");
uItem->anchor()->insertWidget(0, std::move(iconText));

uItem->triggered().connect([this](){
themeSelect.exec(this->jsRef(), "null", "\""+ Theme::Auto +"\"");
this->setText(Wt::WString(MenuButtonString).arg(autoIconString));
s_themeChanged(Theme::Auto);
});

popMenu->addItem(std::move(uItem));
}
}

void ThemeToggleButton::setupConnections()
{
selected.connect([this](std::string ts){
// should only get called in the beginning to set current theme from localStorage
if(Theme::Light == ts)
{
setText(Wt::WString(MenuButtonString).arg(lightIconString));
s_themeChanged(Theme::Light);
Wt::log("debug") << "Bootstrap5::Theme" << "::changed to Theme::Light, aka: " << Theme::Light;
}
else if(Theme::Dark == ts)
{
setText(Wt::WString(MenuButtonString).arg(darkIconString));
s_themeChanged(Theme::Dark);
Wt::log("debug") << "Bootstrap5::Theme" << "::changed to Theme::Dark, aka: " << Theme::Dark;
}
else // Auto
{
setText(Wt::WString(MenuButtonString).arg(autoIconString));
s_themeChanged(Theme::Auto);
Wt::log("debug") << "Bootstrap5::Theme" << "::changed to Theme::Auto, aka: " << Theme::Auto;
}
});
}


} // end namespace Bootstrap5
(1-1/2)