From bb7d178bf8615fc242e887b7b8a2e7881d376326 Mon Sep 17 00:00:00 2001 From: Sunshine Date: Sun, 25 Oct 2026 14:17:20 -0400 Subject: [PATCH] make app window react in real time to appearance changes in settings --- inc/liquidappconfigwindow.hpp | 9 ++ inc/liquidappwindow.hpp | 6 ++ inc/lqd.h | 8 ++ src/liquidappconfigwindow.cpp | 103 ++++++++++++++++--- src/liquidappwindow.cpp | 182 +++++++++++++++++++++++++++------- src/main.cpp | 14 +++ 6 files changed, 276 insertions(+), 46 deletions(-) diff --git a/inc/liquidappconfigwindow.hpp b/inc/liquidappconfigwindow.hpp index 1679d3b..acef98c 100644 --- a/inc/liquidappconfigwindow.hpp +++ b/inc/liquidappconfigwindow.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -24,15 +25,23 @@ class LiquidAppConfigDialog : public QDialog public slots: void save(); + void reject() override; private: void bindShortcuts(void); static QFrame* separator(void); static QString getSettingsRequiringRestart(QSettings* liquidAppConfig); + void previewBackground(const QColor& color); + void sendPreview(const QByteArray& message); bool isEditingExistingBool = false; bool isPlanningToRunBool = false; + // Connection to the Liquid app being edited (if running), for previewing changes + QLocalSocket* liquidAppConnection = Q_NULLPTR; + bool hasPreviewedChanges = false; + QTimer* cssPreviewTimer = Q_NULLPTR; + QAction* quitAction; QColor* backgroundColor; diff --git a/inc/liquidappwindow.hpp b/inc/liquidappwindow.hpp index a0e36f5..de23d4e 100644 --- a/inc/liquidappwindow.hpp +++ b/inc/liquidappwindow.hpp @@ -37,6 +37,10 @@ public slots: void loadFinished(bool ok); void loadStarted(void); void reloadLiquidAppConfig(void); + void setBackground(const QColor& color); + void setAdditionalCss(const QString& css); + void setFrameless(const bool frameless); + void setScrollBarsHidden(const bool hidden); void onIconChanged(QIcon icon); void stopLoadingOrExitFullScreenMode(void); void takeSnapshotSlot(void); @@ -137,7 +141,9 @@ public slots: void saveLiquidAppConfig(void); void applyAllowedDomains(void); void applyBackgroundColor(void); + void recreateNativeWindow(void); void applyWebSettings(void); void applyAdditionalCssAndJs(void); + static QString getAdditionalCssScript(const QString& css); void setupContextMenu(void); }; diff --git a/inc/lqd.h b/inc/lqd.h index b343b6e..17e4b33 100644 --- a/inc/lqd.h +++ b/inc/lqd.h @@ -32,8 +32,16 @@ #define LQD_INSTANCE_MSG_RAISE "raise" #define LQD_INSTANCE_MSG_QUIT "quit" // Liquid apps only #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_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_STARTED "started" // Main window only, followed by a space and Liquid app's name #define LQD_ACTIVATION_TOKEN_TIMEOUT 500 // Milliseconds to wait for the compositor to issue an activation token +#define LQD_PREVIEW_CONNECT_TIMEOUT 100 // Milliseconds to wait for connecting to a Liquid app for previewing settings +#define LQD_RECREATED_WINDOW_RAISE_DELAY 100 // Milliseconds to wait before bringing a re-created native window to front +#define LQD_SCROLLBARS_RELAYOUT_DELAY 100 // Milliseconds to keep the document unscrollable for, to make it redo its scrollbars +#define LQD_CSS_PREVIEW_DELAY 150 // Milliseconds to wait after the last keystroke before previewing CSS #define LQD_INSTANCE_STOP_TIMEOUT 5000 // Milliseconds to wait for a Liquid app to quit /* Names of scripts Liquid injects into web pages */ diff --git a/src/liquidappconfigwindow.cpp b/src/liquidappconfigwindow.cpp index d8a88cb..f85a6a3 100644 --- a/src/liquidappconfigwindow.cpp +++ b/src/liquidappconfigwindow.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -320,6 +321,11 @@ LiquidAppConfigDialog::LiquidAppConfigDialog(QWidget* parent, QString liquidAppN } } + // Show or hide scrollbars in the running Liquid app right away + connect(hideScrollBarsCheckBox, &QCheckBox::toggled, this, [this](const bool isChecked) { + sendPreview(QByteArray(LQD_INSTANCE_MSG_PREVIEW_SCROLL " ") + ((isChecked) ? "off" : "on")); + }); + appearanceTabWidgetLayout->addWidget(hideScrollBarsCheckBox); } #endif @@ -339,6 +345,11 @@ LiquidAppConfigDialog::LiquidAppConfigDialog(QWidget* parent, QString liquidAppN } } + // Remove or bring back the running Liquid app's window frame right away + connect(removeWindowFrameCheckBox, &QCheckBox::toggled, this, [this](const bool isChecked) { + sendPreview(QByteArray(LQD_INSTANCE_MSG_PREVIEW_FRAME " ") + ((isChecked) ? "off" : "on")); + }); + appearanceTabWidgetLayout->addWidget(removeWindowFrameCheckBox); } @@ -378,16 +389,29 @@ LiquidAppConfigDialog::LiquidAppConfigDialog(QWidget* parent, QString liquidAppN customBackgroundColorButtonLayout->addWidget(customBackgroundColorButton); connect(customBackgroundColorButton, &QPushButton::clicked, [&]() { - const QColorDialog::ColorDialogOptions options = QColorDialog::ShowAlphaChannel; - QColor color = QColorDialog::getColor(*backgroundColor, this, tr("Pick custom background color"), options); + QColorDialog colorDialog(*backgroundColor, this); + colorDialog.setWindowTitle(tr("Pick custom background color")); + colorDialog.setOptions(QColorDialog::ShowAlphaChannel); - if (color.isValid()) { - if (!useCustomBackgroundCheckBox->isChecked()) { - useCustomBackgroundCheckBox->setChecked(true); - } + // Show colors in the running Liquid app as they're being picked + connect(&colorDialog, &QColorDialog::currentColorChanged, this, [this](const QColor& color) { + previewBackground(color); + }); + + if (colorDialog.exec() == QDialog::Accepted && colorDialog.selectedColor().isValid()) { + const QColor color = colorDialog.selectedColor(); *backgroundColor = color; customBackgroundColorButton->setStyleSheet(buttonStyle.arg(backgroundColor->name(QColor::HexArgb)).arg(fontSize)); + + if (!useCustomBackgroundCheckBox->isChecked()) { + useCustomBackgroundCheckBox->setChecked(true); // Previews the color too + } else { + previewBackground(color); + } + } else if (hasPreviewedChanges) { + // Picking got cancelled, go back to previewing whatever was there before + previewBackground((useCustomBackgroundCheckBox->isChecked()) ? *backgroundColor : QColor()); } }); } @@ -398,6 +422,10 @@ LiquidAppConfigDialog::LiquidAppConfigDialog(QWidget* parent, QString liquidAppN bool enabledInConfig = existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_USE_CUSTOM_BG, false).toBool(); useCustomBackgroundCheckBox->setChecked(enabledInConfig); } + + connect(useCustomBackgroundCheckBox, &QCheckBox::toggled, this, [this](const bool isChecked) { + previewBackground((isChecked) ? *backgroundColor : QColor()); + }); } // Additional CSS text area @@ -413,10 +441,22 @@ LiquidAppConfigDialog::LiquidAppConfigDialog(QWidget* parent, QString liquidAppN additionalCssTextArea->setPlaceholderText(tr("/* put your custom CSS here */")); connect(additionalCssTextArea, &QPlainTextEdit::textChanged, [=]{ style()->polish(additionalCssTextArea); }); + // Repaint the running Liquid app as its CSS is being edited (shortly after typing stops) + cssPreviewTimer = new QTimer(this); + cssPreviewTimer->setSingleShot(true); + cssPreviewTimer->setInterval(LQD_CSS_PREVIEW_DELAY); + connect(cssPreviewTimer, &QTimer::timeout, this, [this]() { + // Base64, since messages are newline-terminated and CSS may have line breaks in it + sendPreview(QByteArray(LQD_INSTANCE_MSG_PREVIEW_CSS " ") + additionalCssTextArea->toPlainText().toUtf8().toBase64()); + }); + if (isEditingExistingBool) { additionalCssTextArea->setPlainText(existingLiquidAppConfig->value(LQD_CFG_KEY_NAME_ADDITIONAL_CSS).toString()); } + // Connected only now, to not preview what's been loaded from the file + connect(additionalCssTextArea, &QPlainTextEdit::textChanged, cssPreviewTimer, QOverload<>::of(&QTimer::start)); + appearanceTabWidgetLayout->addWidget(additionalCssTextArea); } } @@ -1153,6 +1193,9 @@ void LiquidAppConfigDialog::save() tempLiquidAppConfig->sync(); + // A pending CSS preview is no longer needed, the saved CSS gets applied below + cssPreviewTimer->stop(); + // Apply new settings to the Liquid app if it's running if (isEditingExistingBool) { const QString instanceId = LQD_INSTANCE_ID_APP_PREFIX + appName; @@ -1181,10 +1224,50 @@ void LiquidAppConfigDialog::save() accept(); } +void LiquidAppConfigDialog::previewBackground(const QColor& color) +{ + const QByteArray colorName = (color.isValid()) ? color.name(QColor::HexArgb).toUtf8() : QByteArray("none"); + + sendPreview(QByteArray(LQD_INSTANCE_MSG_PREVIEW_BG " ") + colorName); +} + +void LiquidAppConfigDialog::sendPreview(const QByteArray& message) +{ + // Only applies to Liquid apps that already exist (and are running) + if (!isEditingExistingBool) { + return; + } + + if (liquidAppConnection == Q_NULLPTR) { + liquidAppConnection = new QLocalSocket(this); + liquidAppConnection->connectToServer(SingleInstance::serverName(LQD_INSTANCE_ID_APP_PREFIX + nameInput->text())); + liquidAppConnection->waitForConnected(LQD_PREVIEW_CONNECT_TIMEOUT); + } + + if (liquidAppConnection->state() == QLocalSocket::ConnectedState) { + liquidAppConnection->write(message + "\n"); + liquidAppConnection->flush(); + hasPreviewedChanges = true; + } +} + +void LiquidAppConfigDialog::reject() +{ + // Don't let a pending CSS preview arrive after the settings have been restored + cssPreviewTimer->stop(); + + // Bring back what's been previewed so far to how it is in the saved settings + if (hasPreviewedChanges && liquidAppConnection != Q_NULLPTR && liquidAppConnection->state() == QLocalSocket::ConnectedState) { + liquidAppConnection->write(LQD_INSTANCE_MSG_RELOAD_SETTINGS "\n"); + liquidAppConnection->flush(); + } + + QDialog::reject(); +} + QString LiquidAppConfigDialog::getSettingsRequiringRestart(QSettings* liquidAppConfig) { - // Proxy settings get picked up by the web engine only upon start, - // and window transparency can't be turned on or off once the window is shown + // Proxy settings get picked up by the web engine only upon start static const QStringList keys = { LQD_CFG_KEY_NAME_USE_PROXY, LQD_CFG_KEY_NAME_PROXY_USE_SOCKS, @@ -1200,10 +1283,6 @@ QString LiquidAppConfigDialog::getSettingsRequiringRestart(QSettings* liquidAppC values << ((liquidAppConfig->contains(key)) ? liquidAppConfig->value(key).toString() : QStringLiteral("")); } - const bool isTranslucent = liquidAppConfig->value(LQD_CFG_KEY_NAME_USE_CUSTOM_BG, false).toBool() - && QColor(liquidAppConfig->value(LQD_CFG_KEY_NAME_CUSTOM_BG_COLOR).toString()).alpha() < 255; - values << ((isTranslucent) ? "translucent" : "opaque"); - return values.join('\n'); } diff --git a/src/liquidappwindow.cpp b/src/liquidappwindow.cpp index 29eec7d..e94468e 100644 --- a/src/liquidappwindow.cpp +++ b/src/liquidappwindow.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include #include @@ -636,19 +638,8 @@ void LiquidAppWindow::loadLiquidAppConfig(void) } } - // Set the page's background color behind the document's body - { - if (liquidAppConfig->value(LQD_CFG_KEY_NAME_USE_CUSTOM_BG, false).toBool() && liquidAppConfig->contains(LQD_CFG_KEY_NAME_CUSTOM_BG_COLOR)) { - const QColor backgroundColor = QColor(liquidAppConfig->value(LQD_CFG_KEY_NAME_CUSTOM_BG_COLOR).toString()); - - if (backgroundColor.alpha() < 255) { - // Make window background transparent (can only be done before the window is shown) - setAttribute(Qt::WA_TranslucentBackground, true); - } - } - - applyBackgroundColor(); - } + // Set the page's background color behind the document's body (and make the window translucent if needed) + applyBackgroundColor(); // Determine where this Liquid app is allowed to navigate, and what should be opened in external browser applyAllowedDomains(); @@ -770,11 +761,61 @@ void LiquidAppWindow::applyAllowedDomains(void) void LiquidAppWindow::applyBackgroundColor(void) { - // Set the page's background color behind the document's body if (liquidAppConfig->value(LQD_CFG_KEY_NAME_USE_CUSTOM_BG, false).toBool() && liquidAppConfig->contains(LQD_CFG_KEY_NAME_CUSTOM_BG_COLOR)) { - page()->setBackgroundColor(QColor(liquidAppConfig->value(LQD_CFG_KEY_NAME_CUSTOM_BG_COLOR).toString())); + setBackground(QColor(liquidAppConfig->value(LQD_CFG_KEY_NAME_CUSTOM_BG_COLOR).toString())); } else { - page()->setBackgroundColor(LQD_DEFAULT_BG_COLOR); + setBackground(QColor()); + } +} + +void LiquidAppWindow::setBackground(const QColor& color) +{ + // Invalid color means the default one + const QColor backgroundColor = (color.isValid()) ? color : QColor(LQD_DEFAULT_BG_COLOR); + const bool needsTranslucency = backgroundColor.alpha() < 255; + + // Set the page's background color behind the document's body + page()->setBackgroundColor(backgroundColor); + + // Whether the window can be see-through or not gets decided upon creation of its native window, + // so if that needs to change for a window that's already shown, its native window has to be recreated + if (testAttribute(Qt::WA_TranslucentBackground) != needsTranslucency) { + setAttribute(Qt::WA_TranslucentBackground, needsTranslucency); + if (!needsTranslucency) { + setAttribute(Qt::WA_NoSystemBackground, false); // Gets set along with translucency, but not unset + } + + if (isVisible()) { + recreateNativeWindow(); + } + } + +#ifdef Q_OS_MAC + if (isVisible()) { + SetWindowBackgroundColor(effectiveWinId(), backgroundColor.redF(), backgroundColor.greenF(), backgroundColor.blueF(), backgroundColor.alphaF()); + } +#endif +} + +void LiquidAppWindow::recreateNativeWindow(void) +{ + // Re-applying window flags makes Qt replace the native window (which also hides it); the web view and the page + // it shows remain intact (on some platforms, e.g. Wayland, the window may end up at a different position though) + const Qt::WindowStates states = windowState(); + + setWindowFlags(windowFlags()); + setWindowState(states); + show(); + + // The new native window doesn't come up on top of others by itself (window managers treat it as one that just + // appeared on its own, out of nowhere, while the user is busy with another window), so bring it to front the same way + // a double-click in the main window does, once the window manager has had a moment to put the new one on screen + if (!states.testFlag(Qt::WindowMinimized)) { + QTimer::singleShot(LQD_RECREATED_WINDOW_RAISE_DELAY, this, [this]() { + if (isVisible() && !isMinimized()) { + Liquid::raiseWindow(this, QString()); + } + }); } } @@ -783,8 +824,7 @@ void LiquidAppWindow::applyWebSettings(void) settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, liquidAppConfig->value(LQD_CFG_KEY_NAME_ENABLE_JS, false).toBool()); - settings()->setAttribute(QWebEngineSettings::ShowScrollBars, - !liquidAppConfig->value(LQD_CFG_KEY_NAME_HIDE_SCROLLBARS, false).toBool()); + setScrollBarsHidden(liquidAppConfig->value(LQD_CFG_KEY_NAME_HIDE_SCROLLBARS, false).toBool()); const QString customUserAgent = liquidAppConfig->value(LQD_CFG_KEY_NAME_USER_AGENT).toString(); liquidAppWebProfile->setHttpUserAgent((customUserAgent.isEmpty()) ? defaultHttpUserAgent : customUserAgent); @@ -808,18 +848,11 @@ void LiquidAppWindow::applyAdditionalCssAndJs(void) // Additional user-defined CSS (does't require JavaScript enabled in order to work) if (liquidAppConfig->contains(LQD_CFG_KEY_NAME_ADDITIONAL_CSS)) { - QString additionalCss = liquidAppConfig->value(LQD_CFG_KEY_NAME_ADDITIONAL_CSS).toString(); - const QString js = QString("(()=>{"\ - "const styleEl = document.createElement('style');"\ - "const cssTextNode = document.createTextNode('%1');"\ - "styleEl.appendChild(cssTextNode);"\ - "document.head.appendChild(styleEl)"\ - "})();").arg(additionalCss.replace("\n", " ").replace("'", "\\'")); QWebEngineScript script; script.setName(LQD_SCRIPT_NAME_ADDITIONAL_CSS); script.setInjectionPoint(QWebEngineScript::DocumentReady); script.setRunsOnSubFrames(false); - script.setSourceCode(js); + script.setSourceCode(getAdditionalCssScript(liquidAppConfig->value(LQD_CFG_KEY_NAME_ADDITIONAL_CSS).toString())); script.setWorldId(QWebEngineScript::ApplicationWorld); scripts.insert(script); } @@ -836,13 +869,36 @@ void LiquidAppWindow::applyAdditionalCssAndJs(void) } } +QString LiquidAppWindow::getAdditionalCssScript(const QString& css) +{ + // Puts the CSS into a