diff --git a/src/AppearanceManager.cpp b/src/AppearanceManager.cpp index e30664a..489abb0 100644 --- a/src/AppearanceManager.cpp +++ b/src/AppearanceManager.cpp @@ -17,6 +17,12 @@ namespace { +const QPalette &initialApplicationPalette() +{ + static const QPalette palette = QApplication::palette(); + return palette; +} + double relativeLuminance(const QColor &color) { auto linearChannel = [](int channel) { @@ -46,6 +52,7 @@ AppearanceManager::AppearanceManager(ApplicationSettings *settings, }) , mode(modeFromString(settings->appearance())) , effective(EffectiveAppearance::Light) + , lightPalette(initialApplicationPalette()) { AppearanceTrace::initialize(); if (mode == Mode::System) @@ -197,6 +204,16 @@ void AppearanceManager::restoreSystemPalette() hasSystemPaletteSnapshot = true; } +void AppearanceManager::restoreLightPalette() +{ + if (QApplication::palette() == lightPalette) + return; + + applyingPalette = true; + QApplication::setPalette(lightPalette); + applyingPalette = false; +} + void AppearanceManager::applyApplicationAppearance() { if (mode == Mode::System) { @@ -206,6 +223,11 @@ void AppearanceManager::applyApplicationAppearance() } currentTokens = effective == EffectiveAppearance::Dark ? darkTokens() : lightTokens(); + if (effective == EffectiveAppearance::Light) { + restoreLightPalette(); + return; + } + applyingPalette = true; QApplication::setPalette(explicitPalette(currentTokens)); applyingPalette = false; @@ -328,7 +350,7 @@ AppearanceTokens AppearanceManager::lightTokens() QColor(0, 120, 212, 128), QColor(128, 128, 128, 80), QColor(QStringLiteral("#A1260D")), QColor(QStringLiteral("#8A6D00")), QColor(QStringLiteral("#107C10")), QColor(QStringLiteral("#0067B8")), - QColor(QStringLiteral("#000000")), QColor(QStringLiteral("#FFF4CE")), + QColor(QStringLiteral("#000000")), QColor(QStringLiteral("#E8E8FF")), QColor(QStringLiteral("#A0A0A0")), QColor(QStringLiteral("#C0C0C0")), QColor(QStringLiteral("#707070")), QColor(QStringLiteral("#0078D4")), QColor(QStringLiteral("#A1260D")), QColor(QStringLiteral("#008000")), diff --git a/src/AppearanceManager.h b/src/AppearanceManager.h index 6b9aa92..fdf5fc3 100644 --- a/src/AppearanceManager.h +++ b/src/AppearanceManager.h @@ -107,6 +107,7 @@ public slots: EffectiveAppearance resolveEffectiveAppearance() const; EffectiveAppearance paletteFallback() const; void restoreSystemPalette(); + void restoreLightPalette(); void applyApplicationAppearance(); void applyNativeAppearance(); void applyNativeAppearance(QWidget *window); @@ -124,6 +125,7 @@ public slots: Mode mode; EffectiveAppearance effective; AppearanceTokens currentTokens; + QPalette lightPalette; QPalette systemPaletteSnapshot; bool hasSystemPaletteSnapshot = false; bool applyingPalette = false; diff --git a/src/dialogs/MainWindow.cpp b/src/dialogs/MainWindow.cpp index e9c924b..241a9ac 100644 --- a/src/dialogs/MainWindow.cpp +++ b/src/dialogs/MainWindow.cpp @@ -2011,7 +2011,10 @@ void MainWindow::applyStyleSheet() bool hasCustomStyleSheet = false; { AppearanceTrace::Scope readTrace(QStringLiteral("stylesheet-read")); - QFile f(":/stylesheets/npp.css"); + const QString styleSheetPath = app->getAppearanceManager()->isDark() + ? QStringLiteral(":/stylesheets/npp.css") + : QStringLiteral(":/stylesheets/npp-light.css"); + QFile f(styleSheetPath); qInfo() << "Loading stylesheet:" << f.fileName(); f.open(QFile::ReadOnly); diff --git a/src/resources.qrc b/src/resources.qrc index 4929146..b7245cd 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -19,6 +19,7 @@ icons/wrap.png icons/find.png stylesheets/npp.css + stylesheets/npp-light.css icons/findReplace.png icons/readonly.png icons/startRecord.png diff --git a/src/stylesheets/npp-light.css b/src/stylesheets/npp-light.css new file mode 100644 index 0000000..5b70e2f --- /dev/null +++ b/src/stylesheets/npp-light.css @@ -0,0 +1,77 @@ +QStatusBar { + border-top: 1px solid darkgray; +} + +ads--CDockWidgetTab ads--CElidingLabel { + color : black; +} + +ads--CDockWidgetTab { + background: rgb(192, 192, 192); + margin-top: 2px; + border-left: 1px solid white; + border-top: 2px solid white; + border-right: 2px solid gray; + padding: 2px 2px 0px 2px; + border-top-left-radius: 2px; + border-top-right-radius: 2px; +} + +ads--CDockWidgetTab[activeTab="true"] +{ + background: rgb(240, 240, 240); + border-top: 4px solid rgb(255, 202, 176); + padding-bottom: 2px; + margin-top: 1px; +} + +ads--CDockWidgetTab[focused="true"] +{ + background: rgb(240, 240, 240); + border-top: 4px solid orange; + padding-bottom: 2px; + margin-top: 1px; +} + +ads--CDockWidgetTab:hover[activeTab="false"] { + background: rgb(210, 210, 210); +} + +ads--CDockAreaTitleBar +{ + background: transparent; + border-bottom: 1px solid darkgray; + padding-bottom: 0px; +} + +ads--CDockWidgetTab[activeTab="false"] QLabel { + color: gray; +} + +#tabCloseButton +{ + background: rgba(0, 0, 0, 16); + margin-bottom: 1px; + margin-left: 4px; + border: none; + padding: 0px -2px; +} + +#tabCloseButton:hover +{ + border: 1px solid rgba(0, 0, 0, 32); + background-color: rgb(251, 115, 115); + color: green; +} + +#tabCloseButton:pressed +{ + background: red; +} + +#QuickFindWidget { + border-left: 1px solid darkgray; + border-right: 1px solid darkgray; + border-bottom: 3px solid blue; + background: palette(window); +} \ No newline at end of file diff --git a/src/stylesheets/npp.css b/src/stylesheets/npp.css index 35ace27..f9a7983 100644 --- a/src/stylesheets/npp.css +++ b/src/stylesheets/npp.css @@ -13,7 +13,9 @@ QMenuBar::item:selected, QMenu::item:selected { } QMenu::separator { + height: 1px; background: palette(mid); + margin: 4px 6px; } QToolTip { @@ -22,6 +24,25 @@ QToolTip { border: 1px solid palette(mid); } +QMessageBox { + background: palette(window); +} + +QMessageBox QLabel { + background: transparent; + color: palette(window-text); +} + +QMessageBox QDialogButtonBox { + background: palette(button); + border-top: 1px solid palette(mid); +} + +QMessageBox QPushButton { + min-width: 72px; + padding: 5px 12px; +} + QLineEdit, QComboBox, QSpinBox, QDoubleSpinBox, QTextEdit, QPlainTextEdit, QListView, QTreeView, QTableView { background: palette(base); diff --git a/tests/AppearanceManagerTests.cpp b/tests/AppearanceManagerTests.cpp index 6bcbea8..eeb0f63 100644 --- a/tests/AppearanceManagerTests.cpp +++ b/tests/AppearanceManagerTests.cpp @@ -45,6 +45,7 @@ class AppearanceManagerTests : public QObject private slots: void parsesOnlyCanonicalWireValues(); void defaultsMissingSettingsToClassicLight(); + void explicitLightRestoresNativePalette(); void defaultsInvalidSettingsToSystem(); void persistsExplicitMode(); void emitsOnlyWhenEffectiveAppearanceChanges(); @@ -76,10 +77,36 @@ void AppearanceManagerTests::defaultsMissingSettingsToClassicLight() ApplicationSettings settings(directory.filePath(QStringLiteral("settings.ini")), QSettings::IniFormat); QCOMPARE(settings.appearance(), QStringLiteral("light")); + QVERIFY(!QApplication::testAttribute(Qt::AA_SetPalette)); AppearanceManager manager(&settings, [] { return Qt::ColorScheme::Dark; }); QCOMPARE(manager.requestedMode(), AppearanceManager::Mode::Light); QCOMPARE(manager.effectiveAppearance(), AppearanceManager::EffectiveAppearance::Light); + QVERIFY(!QApplication::testAttribute(Qt::AA_SetPalette)); +} + +void AppearanceManagerTests::explicitLightRestoresNativePalette() +{ + QTemporaryDir directory; + QVERIFY(directory.isValid()); + + const QPalette nativePalette = QApplication::palette(); + ApplicationSettings settings(directory.filePath(QStringLiteral("settings.ini")), QSettings::IniFormat); + AppearanceManager manager(&settings, [] { return Qt::ColorScheme::Light; }); + + manager.setRequestedMode(AppearanceManager::Mode::Dark); + QVERIFY(QApplication::palette() != nativePalette); + + manager.setRequestedMode(AppearanceManager::Mode::Light); + const QPalette actualPalette = QApplication::palette(); + for (QPalette::ColorGroup group : {QPalette::Active, QPalette::Inactive, QPalette::Disabled}) { + for (int role = 0; role < QPalette::NColorRoles; ++role) { + if (role == QPalette::NoRole) + continue; + QCOMPARE(actualPalette.color(group, static_cast(role)), + nativePalette.color(group, static_cast(role))); + } + } } void AppearanceManagerTests::defaultsInvalidSettingsToSystem() diff --git a/tests/CompareSessionTests.cpp b/tests/CompareSessionTests.cpp index 4c03583..9e4edec 100644 --- a/tests/CompareSessionTests.cpp +++ b/tests/CompareSessionTests.cpp @@ -21,9 +21,14 @@ #include #include #include +#include #include #include +#include +#include #include +#include +#include #include #include #include @@ -40,6 +45,122 @@ class CompareSessionTests : public QObject Q_OBJECT private slots: + void lightStylePreservesNativeControls() + { + QFile styleSheet(QStringLiteral(":/stylesheets/npp-light.css")); + QVERIFY(styleSheet.open(QIODevice::ReadOnly | QIODevice::Text)); + const QString css = QString::fromUtf8(styleSheet.readAll()) + + DockedEditor::tabTitleStyleSheet(Qt::black, Qt::gray); + + QWidget nativeHost; + QWidget lightHost; + lightHost.setStyleSheet(css); + + QMessageBox nativeMessageBox(&nativeHost); + QMessageBox lightMessageBox(&lightHost); + auto addButtons = [](QMessageBox &messageBox) { + QPushButton *saveButton = messageBox.addButton( + QStringLiteral("Save"), QMessageBox::AcceptRole); + messageBox.addButton(QStringLiteral("Discard"), QMessageBox::DestructiveRole); + messageBox.addButton(QMessageBox::Cancel); + return saveButton; + }; + QPushButton *nativeSaveButton = addButtons(nativeMessageBox); + QPushButton *lightSaveButton = addButtons(lightMessageBox); + + QMenu nativeMenu(&nativeHost); + QMenu lightMenu(&lightHost); + nativeMenu.addAction(QStringLiteral("First")); + QAction *nativeSeparator = nativeMenu.addSeparator(); + nativeMenu.addAction(QStringLiteral("Second")); + lightMenu.addAction(QStringLiteral("First")); + QAction *lightSeparator = lightMenu.addSeparator(); + lightMenu.addAction(QStringLiteral("Second")); + + nativeMessageBox.show(); + lightMessageBox.show(); + nativeMenu.show(); + lightMenu.show(); + QCoreApplication::processEvents(); + + QCOMPARE(lightSaveButton->size(), nativeSaveButton->size()); + QCOMPARE(lightMenu.actionGeometry(lightSeparator).height(), + nativeMenu.actionGeometry(nativeSeparator).height()); + } + + void darkStyleKeepsMenuSeparatorsCompact() + { + QFile styleSheet(QStringLiteral(":/stylesheets/npp.css")); + QVERIFY(styleSheet.open(QIODevice::ReadOnly | QIODevice::Text)); + + QWidget host; + QPalette palette = host.palette(); + palette.setColor(QPalette::Window, QColor(QStringLiteral("#101010"))); + palette.setColor(QPalette::Mid, QColor(QStringLiteral("#5A5A5A"))); + host.setPalette(palette); + host.setStyleSheet(QString::fromUtf8(styleSheet.readAll())); + QMenu menu(&host); + menu.addAction(QStringLiteral("First")); + QAction *separator = menu.addSeparator(); + menu.addAction(QStringLiteral("Second")); + menu.show(); + QCoreApplication::processEvents(); + + QImage image(menu.size(), QImage::Format_ARGB32); + image.fill(Qt::transparent); + menu.render(&image); + + const QRect separatorGeometry = menu.actionGeometry(separator); + const int sampleX = separatorGeometry.center().x(); + const QColor backgroundColor = image.pixelColor(sampleX, separatorGeometry.top()); + int separatorRows = 0; + for (int y = separatorGeometry.top(); y <= separatorGeometry.bottom(); ++y) { + if (image.pixelColor(sampleX, y) != backgroundColor) + ++separatorRows; + } + QCOMPARE(separatorRows, 1); + } + + void darkStyleKeepsMessageBoxesDark() + { + QFile styleSheet(QStringLiteral(":/stylesheets/npp.css")); + QVERIFY(styleSheet.open(QIODevice::ReadOnly | QIODevice::Text)); + + QWidget host; + QPalette darkPalette = host.palette(); + darkPalette.setColor(QPalette::Window, QColor(QStringLiteral("#191A1B"))); + darkPalette.setColor(QPalette::WindowText, QColor(QStringLiteral("#BFBFBF"))); + darkPalette.setColor(QPalette::Button, QColor(QStringLiteral("#202122"))); + darkPalette.setColor(QPalette::ButtonText, QColor(QStringLiteral("#BFBFBF"))); + darkPalette.setColor(QPalette::Mid, QColor(QStringLiteral("#333536"))); + const QPalette originalPalette = QApplication::palette(); + const auto restorePalette = qScopeGuard([originalPalette] { + QApplication::setPalette(originalPalette); + }); + QApplication::setPalette(darkPalette); + host.setPalette(darkPalette); + host.setStyleSheet(QString::fromUtf8(styleSheet.readAll())); + + QMessageBox messageBox(&host); + messageBox.setIcon(QMessageBox::Question); + messageBox.setText(QStringLiteral("Save changes to New 1?")); + messageBox.addButton(QStringLiteral("Save"), QMessageBox::AcceptRole); + messageBox.addButton(QStringLiteral("Discard"), QMessageBox::DestructiveRole); + messageBox.addButton(QMessageBox::Cancel); + messageBox.show(); + QCoreApplication::processEvents(); + + QImage image(messageBox.size(), QImage::Format_ARGB32); + image.fill(Qt::transparent); + messageBox.render(&image); + + const QColor bodyPixel = image.pixelColor(messageBox.width() / 2, 12); + const QColor footerPixel = image.pixelColor(12, messageBox.height() - 12); + QCOMPARE(bodyPixel, darkPalette.color(QPalette::Window)); + QVERIFY(footerPixel.lightness() < 80); + QVERIFY(messageBox.buttons().first()->width() >= 72); + } + void darkStyleKeepsInactiveTabTitlesReadable() { QFile styleSheet(QStringLiteral(":/stylesheets/npp.css")); diff --git a/tests/EditorAppearanceTests.cpp b/tests/EditorAppearanceTests.cpp index 1eb3ff1..ac7376b 100644 --- a/tests/EditorAppearanceTests.cpp +++ b/tests/EditorAppearanceTests.cpp @@ -95,6 +95,8 @@ void EditorAppearanceTests::switchesTheSameEditorBackToLight() EditorAppearance::apply(&editor, manager.tokens(), QStringLiteral("Courier New"), 11); QCOMPARE(editor.styleBack(STYLE_DEFAULT), static_cast(AppearanceManager::scintillaColor(QColor(QStringLiteral("#FFFFFF"))))); + QCOMPARE(static_cast(editor.elementColour(SC_ELEMENT_CARET_LINE_BACK)), + 0xFFFFE8E8u); } void EditorAppearanceTests::preservesExistingLexerStyleAssignments()