Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions inc/liquid.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ class Liquid
static void applyQtStyleSheets(QWidget* widget);
static void createDesktopFile(const QString liquidAppName, const QString liquidAppStartingUrl);
static bool detectDarkMode(void);
// Makes detectDarkMode() tell what's been chosen (e.g. for a Liquid app), rather than what the system uses
static void overrideDarkMode(const bool isDark);
static void clearDarkModeOverride(void);
static bool detectSystemDarkMode(void);
static bool detectDarkModeUncached(void);
static QByteArray generateRandomByteArray(const int byteLength);
static QDir getAppsDir(void);
Expand Down
1 change: 1 addition & 0 deletions inc/liquidappconfigwindow.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ public slots:
QPlainTextEdit* notesTextArea;

// Appearance tab
QComboBox* colorSchemeComboBox;
QCheckBox* hideScrollBarsCheckBox;
QCheckBox* removeWindowFrameCheckBox;
QCheckBox* useCustomBackgroundCheckBox;
Expand Down
9 changes: 9 additions & 0 deletions inc/liquidappwindow.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public slots:
void setBackground(const QColor& color);
void setAdditionalCss(const QString& css);
void setFrameless(const bool frameless);
// "light", "dark", or anything else for the system's; applies right away (to web pages and Liquid's own windows)
void setColorScheme(const QString& colorScheme);
// Keeps sending session storage of the page that's currently shown over the connection, until it closes
void watchSessionStorage(QLocalSocket* connection);
void removeSessionStorageItems(const QString& origin, const QStringList& keys);
Expand Down Expand Up @@ -102,6 +104,13 @@ public slots:

// Remembers what websites keep in local storage (if allowed to)
LiquidAppLocalStorage* liquidAppLocalStorage = Q_NULLPTR;

// Web pages can only be told which color scheme to use upon start: switching it while running is done by emulating it
// through a hidden DevTools frontend (in-process, nothing gets exposed), kept only while it differs from the initial one
bool isDarkUponStart = false;
QWebEnginePage* colorSchemeDevToolsPage = Q_NULLPTR;
bool isEmulatingDark = false;
void sendColorSchemeEmulation(void);
QList<QPointer<QLocalSocket>> sessionStorageWatchers;
void sendSessionStorage(QLocalSocket* connection);

Expand Down
7 changes: 7 additions & 0 deletions inc/lqd.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#define LQD_INSTANCE_MSG_RELOAD_SETTINGS "reload-settings" // Liquid apps only
#define LQD_INSTANCE_MSG_PREVIEW_BG "preview-background" // Liquid apps only, followed by a space and #AARRGGBB (or "none")
#define LQD_INSTANCE_MSG_PREVIEW_CSS "preview-css" // Liquid apps only, followed by a space and base64-encoded CSS
#define LQD_INSTANCE_MSG_PREVIEW_SCHEME "preview-color-scheme" // Liquid apps only, followed by a space and "light", "dark", or "system"
#define LQD_INSTANCE_MSG_PREVIEW_FRAME "preview-frame" // Liquid apps only, followed by a space and "on" or "off"
#define LQD_INSTANCE_MSG_PREVIEW_SCROLL "preview-scrollbars" // Liquid apps only, followed by a space and "on" or "off"
#define LQD_INSTANCE_MSG_ICON_CHANGED "icon-changed" // Main window only, followed by a space and Liquid app's name
Expand Down Expand Up @@ -65,6 +66,7 @@
#define LQD_FIND_BOX_HEIGHT 42 // Pixels
#define LQD_FIND_BOX_MARGIN 16 // Pixels, between the search box and the edges of small windows
#define LQD_TYPE_TO_FIND_DELAY 50 // Milliseconds to wait for the page to handle typed keys first
#define LQD_COLOR_SCHEME_EMULATION_DELAY 1000 // Milliseconds for the hidden DevTools frontend to settle before switching color schemes
#define LQD_INSTANCE_STOP_TIMEOUT 5000 // Milliseconds to wait for a Liquid app to quit

/* Names of scripts Liquid injects into web pages */
Expand All @@ -85,6 +87,10 @@
#define LQD_ICON_RUN "➤"
#define LQD_ICON_STOP "■"

/* Color schemes */
#define LQD_COLOR_SCHEME_LIGHT "light"
#define LQD_COLOR_SCHEME_DARK "dark"

/* Liquid App config file group names */
#define LQD_CFG_GROUP_NAME_COOKIES "Cookies"
#define LQD_CFG_GROUP_NAME_PROXY "Proxy"
Expand All @@ -101,6 +107,7 @@
#define LQD_CFG_KEY_NAME_ADDITIONAL_JS "AdditionalJS" // text
#define LQD_CFG_KEY_NAME_ALLOW_COOKIES "AllowCookies" // boolean, defults to FALSE
#define LQD_CFG_KEY_NAME_ALLOW_3RD_PARTY_COOKIES "AllowThirdPartyCookies" // boolean, defaults to FALSE
#define LQD_CFG_KEY_NAME_COLOR_SCHEME "ColorScheme" // text: "light" or "dark", system's if unset
#define LQD_CFG_KEY_NAME_CUSTOM_BG_COLOR "CustomBackgroundColor" // text
#define LQD_CFG_KEY_NAME_ENABLE_JS "EnableJS" // boolean, defaults to FALSE
#define LQD_CFG_KEY_NAME_HIDE_SCROLLBARS "HideScrollBars" // boolean, defaults to FALSE
Expand Down
30 changes: 30 additions & 0 deletions src/liquid.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,29 @@ void Liquid::updateDesktopFileIcon(const QString liquidAppName)
#endif
}

// Whether dark mode is on (1) or off (0) for this program, if not up to the system (-1)
static int darkModeOverride = -1;

void Liquid::overrideDarkMode(const bool isDark)
{
darkModeOverride = (isDark) ? 1 : 0;
}

void Liquid::clearDarkModeOverride(void)
{
darkModeOverride = -1;
}

bool Liquid::detectDarkMode(void)
{
if (darkModeOverride >= 0) {
return darkModeOverride == 1;
}

return detectSystemDarkMode();
}

bool Liquid::detectSystemDarkMode(void)
{
// Detecting it may involve spawning a process, do it only once per program run
static int cachedResult = -1;
Expand Down Expand Up @@ -315,6 +337,10 @@ void Liquid::removeDesktopFile(const QString liquidAppName)
#endif
}

// Web engine flags as they were upon start: Liquid apps add their own (e.g. for dark mode), which must not get passed on
// to Liquid apps they run (e.g. upon restarting, with a different color scheme)
static const QByteArray initialChromiumFlags = qgetenv("QTWEBENGINE_CHROMIUM_FLAGS");

void Liquid::runLiquidApp(const QString liquidAppName, const QString activationToken)
{
QProcess process;
Expand All @@ -327,6 +353,10 @@ void Liquid::runLiquidApp(const QString liquidAppName, const QString activationT
if (!activationToken.isEmpty()) {
environment.insert("XDG_ACTIVATION_TOKEN", activationToken);
}
environment.remove("QTWEBENGINE_CHROMIUM_FLAGS");
if (!initialChromiumFlags.isEmpty()) {
environment.insert("QTWEBENGINE_CHROMIUM_FLAGS", QString::fromLocal8Bit(initialChromiumFlags));
}
process.setProcessEnvironment(environment);

process.startDetached();
Expand Down
42 changes: 42 additions & 0 deletions src/liquidappconfigwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,37 @@ LiquidAppConfigDialog::LiquidAppConfigDialog(QWidget* parent, QString liquidAppN
appearanceTabWidget->setLayout(appearanceTabWidgetLayout);
tabWidget->addTab(appearanceTabWidget, tr("Appearance"));

// Color scheme
{
QHBoxLayout* colorSchemeLayout = new QHBoxLayout();

QLabel* colorSchemeLabel = new QLabel(tr("Color scheme:"), this);
colorSchemeLayout->addWidget(colorSchemeLabel);

colorSchemeComboBox = new QComboBox(this);
colorSchemeComboBox->setCursor(Qt::PointingHandCursor);
colorSchemeComboBox->addItem(tr("System"), QString());
colorSchemeComboBox->addItem(tr("Light"), QStringLiteral(LQD_COLOR_SCHEME_LIGHT));
colorSchemeComboBox->addItem(tr("Dark"), QStringLiteral(LQD_COLOR_SCHEME_DARK));

if (isEditingExistingBool) {
const int index = colorSchemeComboBox->findData(existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_COLOR_SCHEME).toString());
colorSchemeComboBox->setCurrentIndex((index >= 0) ? index : 0);
}

// The running Liquid app switches right away
connect(colorSchemeComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this]() {
const QString colorScheme = colorSchemeComboBox->currentData().toString();
sendPreview(QByteArray(LQD_INSTANCE_MSG_PREVIEW_SCHEME " ") + ((colorScheme.isEmpty()) ? QByteArray("system") : colorScheme.toUtf8()));
});

colorSchemeLayout->addWidget(colorSchemeComboBox);
colorSchemeLayout->addStretch();

appearanceTabWidgetLayout->addLayout(colorSchemeLayout);
appearanceTabWidgetLayout->addWidget(separator());
}

// Hide scrollbars checkbox
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
{
Expand Down Expand Up @@ -1122,6 +1153,17 @@ void LiquidAppConfigDialog::save()
}

#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
// Color scheme (the system's, unless chosen otherwise)
{
const QString colorScheme = colorSchemeComboBox->currentData().toString();

if (colorScheme.isEmpty()) {
tempLiquidAppConfig->remove(LQD_CFG_KEY_NAME_COLOR_SCHEME);
} else {
tempLiquidAppConfig->setValue(LQD_CFG_KEY_NAME_COLOR_SCHEME, colorScheme);
}
}

// Hide scrollbars
{
if (isEditingExistingBool) {
Expand Down
1 change: 1 addition & 0 deletions src/liquidappfile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const AttributeMapping ATTRIBUTE_MAPPINGS[] = {
{ LQD_CFG_KEY_NAME_HIDE_SCROLLBARS, "viewport", "hide-scrollbars" },
{ LQD_CFG_KEY_NAME_USE_CUSTOM_BG, "viewport", "custom-background" },
{ LQD_CFG_KEY_NAME_CUSTOM_BG_COLOR, "viewport", "background-color" },
{ LQD_CFG_KEY_NAME_COLOR_SCHEME, "viewport", "color-scheme" },
{ LQD_CFG_KEY_NAME_MUTE_AUDIO, "audio", "muted" },
{ LQD_CFG_KEY_NAME_USE_PROXY, "proxy", "enabled" },
{ LQD_CFG_KEY_NAME_PROXY_USE_SOCKS, "proxy", "socks" },
Expand Down
112 changes: 102 additions & 10 deletions src/liquidappwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <QApplication>
#include <QBuffer>
#include <QDialog>
#include <QDir>
#include <QElapsedTimer>
#include <QJsonArray>
Expand Down Expand Up @@ -125,16 +126,6 @@ LiquidAppWindow::LiquidAppWindow(const QString* name) : QWebEngineView()
qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--num-raster-threads=4");
qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--use-fake-ui-for-media-stream");

#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
// Ensure dark mode is enabled on the web page in case the system theme is dark
if (Liquid::detectDarkMode()) {
qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--force-dark-mode");
qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--blink-settings=forceDarkModeEnabled=true");
qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--blink-settings=darkMode=4");
qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--blink-settings=darkModeEnabled=true");
}
#endif

// Set default icon
#if !defined(Q_OS_LINUX) // This doesn't work on X11
setWindowIcon(QIcon(":/images/" PROG_NAME ".svg"));
Expand All @@ -154,6 +145,27 @@ LiquidAppWindow::LiquidAppWindow(const QString* name) : QWebEngineView()

liquidAppConfig = LiquidAppFile::open(*name);

// Color scheme: the one chosen for this Liquid app, or the system's; it applies to this Liquid app's own windows
// (e.g. its settings), and to web pages (which can only be told upon start, before the web engine gets going)
{
const QString colorScheme = liquidAppConfig->value(LQD_CFG_KEY_NAME_COLOR_SCHEME).toString();

if (colorScheme == LQD_COLOR_SCHEME_DARK || colorScheme == LQD_COLOR_SCHEME_LIGHT) {
Liquid::overrideDarkMode(colorScheme == LQD_COLOR_SCHEME_DARK);
}

isDarkUponStart = Liquid::detectDarkMode();

#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
if (isDarkUponStart) {
qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--force-dark-mode");
qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--blink-settings=forceDarkModeEnabled=true");
qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--blink-settings=darkMode=4");
qputenv("QTWEBENGINE_CHROMIUM_FLAGS", qgetenv("QTWEBENGINE_CHROMIUM_FLAGS") + " " + "--blink-settings=darkModeEnabled=true");
}
#endif
}

// These default settings affect everything (including sub-frames)
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
LiquidAppWebPage::setWebSettingsToDefault(QWebEngineSettings::globalSettings());
Expand Down Expand Up @@ -1063,6 +1075,9 @@ void LiquidAppWindow::reloadLiquidAppConfig(void)
maybeFetchIcon();
}

// Color scheme
setColorScheme(liquidAppConfig->value(LQD_CFG_KEY_NAME_COLOR_SCHEME).toString());

// Local storage (may have been allowed or disallowed to be remembered, or had its items edited)
liquidAppLocalStorage->reloadConfig();

Expand Down Expand Up @@ -2087,3 +2102,80 @@ void LiquidAppWindow::onTypedOutsideOfInputs(QKeyEvent* event)
typeToFindTimer->start();
}
}

void LiquidAppWindow::setColorScheme(const QString& colorScheme)
{
const bool isChosen = colorScheme == LQD_COLOR_SCHEME_DARK || colorScheme == LQD_COLOR_SCHEME_LIGHT;
const bool isDark = (isChosen) ? colorScheme == LQD_COLOR_SCHEME_DARK : Liquid::detectSystemDarkMode();

// Liquid's own windows (e.g. settings opened from this Liquid app)
if (isChosen) {
Liquid::overrideDarkMode(isDark);
} else {
Liquid::clearDarkModeOverride();
}
for (QWidget* widget : QApplication::topLevelWidgets()) {
if (widget == this || qobject_cast<QDialog*>(widget) != Q_NULLPTR) {
Liquid::applyQtStyleSheets(widget);
}
}

// Web pages: as they were told upon start, or emulated
if (isDark == isDarkUponStart) {
if (colorSchemeDevToolsPage != Q_NULLPTR) {
// Detaching ends the emulation
page()->setDevToolsPage(Q_NULLPTR);
colorSchemeDevToolsPage->deleteLater();
colorSchemeDevToolsPage = Q_NULLPTR;
}
return;
}

isEmulatingDark = isDark;

if (colorSchemeDevToolsPage == Q_NULLPTR) {
colorSchemeDevToolsPage = new QWebEnginePage(liquidAppWebProfile, this);
// The DevTools frontend is made of JavaScript (and so is how it gets told what to do)
colorSchemeDevToolsPage->settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, true);

// Once it's loaded (and done setting itself up, which includes resetting emulation), and then once more,
// just in case it's taken longer than usual
connect(colorSchemeDevToolsPage, &QWebEnginePage::loadFinished, this, [this]() {
QTimer::singleShot(LQD_COLOR_SCHEME_EMULATION_DELAY, this, &LiquidAppWindow::sendColorSchemeEmulation);
QTimer::singleShot(LQD_COLOR_SCHEME_EMULATION_DELAY * 3, this, &LiquidAppWindow::sendColorSchemeEmulation);
});

// Loads the DevTools frontend into it, attached to this Liquid app's page
page()->setDevToolsPage(colorSchemeDevToolsPage);
} else {
sendColorSchemeEmulation();
}
}

void LiquidAppWindow::sendColorSchemeEmulation(void)
{
if (colorSchemeDevToolsPage == Q_NULLPTR) {
return;
}

// A DevTools protocol command, passed to the page through the frontend's own (in-process) channel
static int commandId = 1000000; // Far from the frontend's own command IDs
QJsonObject feature;
feature.insert("name", "prefers-color-scheme");
feature.insert("value", (isEmulatingDark) ? "dark" : "light");
QJsonObject params;
params.insert("features", QJsonArray() << feature);
QJsonObject command;
command.insert("id", ++commandId);
command.insert("method", "Emulation.setEmulatedMedia");
command.insert("params", params);

QJsonObject message;
message.insert("id", commandId);
message.insert("method", "dispatchProtocolMessage");
message.insert("params", QJsonArray() << QString::fromUtf8(QJsonDocument(command).toJson(QJsonDocument::Compact)));

const QString quotedMessage = QString::fromUtf8(QJsonDocument(QJsonArray() << QString::fromUtf8(QJsonDocument(message).toJson(QJsonDocument::Compact)))
.toJson(QJsonDocument::Compact)).mid(1).chopped(1);
colorSchemeDevToolsPage->runJavaScript(QString("window.DevToolsHost && DevToolsHost.sendMessageToEmbedder(%1);").arg(quotedMessage));
}
3 changes: 3 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ int main(int argc, char **argv)
// Temporary, while the settings are being edited (reloading settings brings back the saved one)
const QString color = QString::fromUtf8(message.mid(qstrlen(LQD_INSTANCE_MSG_PREVIEW_BG " "))).trimmed();
liquidAppWindow->setBackground((color == "none") ? QColor() : QColor(color));
} else if (message.startsWith(LQD_INSTANCE_MSG_PREVIEW_SCHEME " ")) {
// Temporary, while the settings are being edited (reloading settings brings back the saved one)
liquidAppWindow->setColorScheme(QString::fromUtf8(message.mid(qstrlen(LQD_INSTANCE_MSG_PREVIEW_SCHEME " "))).trimmed());
} else if (message.startsWith(LQD_INSTANCE_MSG_PREVIEW_FRAME " ")) {
// Temporary, while the settings are being edited (reloading settings brings back the saved one)
liquidAppWindow->setFrameless(message.mid(qstrlen(LQD_INSTANCE_MSG_PREVIEW_FRAME " ")).trimmed() == "off");
Expand Down
Loading