diff --git a/src/AppearanceManager.cpp b/src/AppearanceManager.cpp new file mode 100644 index 0000000..4cf0168 --- /dev/null +++ b/src/AppearanceManager.cpp @@ -0,0 +1,431 @@ +#include "AppearanceManager.h" + +#include "AppearanceTrace.h" +#include "ApplicationSettings.h" +#include "WindowsAppearance.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +double relativeLuminance(const QColor &color) +{ + auto linearChannel = [](int channel) { + const double value = channel / 255.0; + return value <= 0.04045 ? value / 12.92 : qPow((value + 0.055) / 1.055, 2.4); + }; + + return 0.2126 * linearChannel(color.red()) + + 0.7152 * linearChannel(color.green()) + + 0.0722 * linearChannel(color.blue()); +} + +} // namespace + +AppearanceManager::AppearanceManager(ApplicationSettings *settings, QObject *parent) + : AppearanceManager(settings, {}, parent) +{ +} + +AppearanceManager::AppearanceManager(ApplicationSettings *settings, + ColorSchemeProvider colorSchemeProvider, QObject *parent) + : QObject(parent) + , settings(settings) + , colorSchemeProvider(colorSchemeProvider ? std::move(colorSchemeProvider) : [] { + const QStyleHints *styleHints = QGuiApplication::styleHints(); + return styleHints ? styleHints->colorScheme() : Qt::ColorScheme::Unknown; + }) + , mode(modeFromString(settings->appearance())) + , effective(EffectiveAppearance::Light) +{ + AppearanceTrace::initialize(); + if (mode == Mode::System) + restoreSystemPalette(); + effective = resolveEffectiveAppearance(); + applyApplicationAppearance(); + qApp->installEventFilter(this); + + connect(settings, &ApplicationSettings::appearanceChanged, + this, &AppearanceManager::onAppearanceSettingChanged); + + if (QStyleHints *styleHints = QGuiApplication::styleHints()) { + connect(styleHints, &QStyleHints::colorSchemeChanged, + this, &AppearanceManager::refreshSystemAppearance); + } +} + +AppearanceManager::Mode AppearanceManager::modeFromString(const QString &value) +{ + if (value == QStringLiteral("light")) + return Mode::Light; + if (value == QStringLiteral("dark")) + return Mode::Dark; + return Mode::System; +} + +QString AppearanceManager::modeToString(Mode mode) +{ + switch (mode) { + case Mode::Light: + return QStringLiteral("light"); + case Mode::Dark: + return QStringLiteral("dark"); + case Mode::System: + default: + return QStringLiteral("system"); + } +} + +AppearanceManager::EffectiveAppearance AppearanceManager::resolve( + Mode mode, Qt::ColorScheme systemScheme, EffectiveAppearance unknownFallback) +{ + if (mode == Mode::Light) + return EffectiveAppearance::Light; + if (mode == Mode::Dark) + return EffectiveAppearance::Dark; + if (systemScheme == Qt::ColorScheme::Dark) + return EffectiveAppearance::Dark; + if (systemScheme == Qt::ColorScheme::Light) + return EffectiveAppearance::Light; + return unknownFallback; +} + +int AppearanceManager::scintillaColor(const QColor &color) +{ + return color.red() | (color.green() << 8) | (color.blue() << 16); +} + +unsigned int AppearanceManager::scintillaElementColor(const QColor &color) +{ + return static_cast(scintillaColor(color)) + | (static_cast(color.alpha()) << 24); +} + +void AppearanceManager::setRequestedMode(Mode requestedMode) +{ + settings->setAppearance(modeToString(requestedMode)); +} + +void AppearanceManager::refreshSystemAppearance() +{ + if (mode != Mode::System) + return; + + QElapsedTimer total; + quint64 cycle = 0; + if (AppearanceTrace::enabled()) { + total.start(); + cycle = AppearanceTrace::beginCycle( + QStringLiteral("system-color-scheme"), + QStringLiteral("requested=system effective-before=%1 windows=%2") + .arg(isDark() ? QStringLiteral("dark") : QStringLiteral("light")) + .arg(QApplication::topLevelWidgets().size())); + } + { + AppearanceTrace::Scope trace(QStringLiteral("restore-system-palette")); + restoreSystemPalette(); + } + updateEffectiveAppearance(true); + if (cycle != 0) { + AppearanceTrace::endCycle(cycle, total.elapsed(), + QStringLiteral("effective-after=%1") + .arg(isDark() ? QStringLiteral("dark") : QStringLiteral("light"))); + } +} + +void AppearanceManager::onAppearanceSettingChanged(const QString &value) +{ + const Mode requestedMode = modeFromString(value); + if (mode == requestedMode) + return; + + QElapsedTimer total; + quint64 cycle = 0; + if (AppearanceTrace::enabled()) { + total.start(); + cycle = AppearanceTrace::beginCycle( + QStringLiteral("setting-change"), + QStringLiteral("requested=%1 previous=%2 effective-before=%3 windows=%4") + .arg(modeToString(requestedMode), modeToString(mode), + isDark() ? QStringLiteral("dark") : QStringLiteral("light")) + .arg(QApplication::topLevelWidgets().size())); + } + mode = requestedMode; + if (mode == Mode::System) { + AppearanceTrace::Scope trace(QStringLiteral("restore-system-palette")); + restoreSystemPalette(); + } + updateEffectiveAppearance(true); + if (cycle != 0) { + AppearanceTrace::endCycle(cycle, total.elapsed(), + QStringLiteral("effective-after=%1") + .arg(isDark() ? QStringLiteral("dark") : QStringLiteral("light"))); + } +} + +AppearanceManager::EffectiveAppearance AppearanceManager::resolveEffectiveAppearance() const +{ + return resolve(mode, colorSchemeProvider(), paletteFallback()); +} + +AppearanceManager::EffectiveAppearance AppearanceManager::paletteFallback() const +{ + const QPalette palette = QApplication::palette(); + return relativeLuminance(palette.color(QPalette::Base)) + < relativeLuminance(palette.color(QPalette::Text)) + ? EffectiveAppearance::Dark + : EffectiveAppearance::Light; +} + +void AppearanceManager::restoreSystemPalette() +{ + if (QApplication::style()) { + applyingPalette = true; + QApplication::setPalette(QApplication::style()->standardPalette()); + applyingPalette = false; + } + systemPaletteSnapshot = QApplication::palette(); + hasSystemPaletteSnapshot = true; +} + +void AppearanceManager::applyApplicationAppearance() +{ + if (mode == Mode::System) { + currentTokens = systemTokens(QApplication::palette(), effective, + WindowsAppearance::isHighContrast()); + return; + } + + currentTokens = effective == EffectiveAppearance::Dark ? darkTokens() : lightTokens(); + applyingPalette = true; + QApplication::setPalette(explicitPalette(currentTokens)); + applyingPalette = false; +} + +bool AppearanceManager::eventFilter(QObject *watched, QEvent *event) +{ + if (event->type() == QEvent::ApplicationPaletteChange + && mode == Mode::System && !applyingPalette && !updatingAppearance) { + const QPalette applicationPalette = QApplication::palette(); + if (hasSystemPaletteSnapshot && applicationPalette == systemPaletteSnapshot) + return QObject::eventFilter(watched, event); + + systemPaletteSnapshot = applicationPalette; + hasSystemPaletteSnapshot = true; + QElapsedTimer total; + quint64 cycle = 0; + if (AppearanceTrace::enabled()) { + total.start(); + cycle = AppearanceTrace::beginCycle( + QStringLiteral("application-palette-change"), + QStringLiteral("requested=system effective-before=%1 windows=%2") + .arg(isDark() ? QStringLiteral("dark") : QStringLiteral("light")) + .arg(QApplication::topLevelWidgets().size())); + } + updateEffectiveAppearance(true); + if (cycle != 0) { + AppearanceTrace::endCycle(cycle, total.elapsed(), + QStringLiteral("effective-after=%1") + .arg(isDark() ? QStringLiteral("dark") : QStringLiteral("light"))); + } + } + else if (event->type() == QEvent::Show || event->type() == QEvent::WinIdChange) { + if (QWidget *widget = qobject_cast(watched); widget && widget->isWindow()) + applyNativeAppearance(widget); + } + + return QObject::eventFilter(watched, event); +} + +void AppearanceManager::applyNativeAppearance() +{ + for (QWidget *window : QApplication::topLevelWidgets()) + applyNativeAppearance(window); +} + +void AppearanceManager::applyNativeAppearance(QWidget *window) +{ + WindowsAppearance::applyToWindow(window, isDark(), mode == Mode::System, + currentTokens.surfaceShell, currentTokens.textPrimary, + currentTokens.borderDefault); +} + +void AppearanceManager::updateEffectiveAppearance(bool forceUpdate) +{ + if (updatingAppearance) + return; + QScopedValueRollback guard(updatingAppearance, true); + + EffectiveAppearance resolved; + { + AppearanceTrace::Scope trace(QStringLiteral("resolve-effective-appearance")); + resolved = resolveEffectiveAppearance(); + } + if (effective == resolved && !forceUpdate) + return; + + effective = resolved; + { + AppearanceTrace::Scope trace(QStringLiteral("application-palette")); + applyApplicationAppearance(); + } + { + AppearanceTrace::Scope trace(QStringLiteral("native-windows"), + QStringLiteral("windows=%1").arg(QApplication::topLevelWidgets().size())); + applyNativeAppearance(); + } + { + AppearanceTrace::Scope trace(QStringLiteral("synchronous-slots")); + emit effectiveAppearanceChanged(effective); + } +} + +AppearanceTokens AppearanceManager::darkTokens() +{ + return { + QColor(QStringLiteral("#121314")), QColor(QStringLiteral("#191A1B")), + QColor(QStringLiteral("#202122")), QColor(QStringLiteral("#242526")), + QColor(QStringLiteral("#333536")), QColor(QStringLiteral("#BFBFBF")), + QColor(QStringLiteral("#BBBEBF")), QColor(QStringLiteral("#8C8C8C")), + QColor(QStringLiteral("#555555")), QColor(QStringLiteral("#297AA0")), + QColor(QStringLiteral("#2B7DA3")), QColor(57, 148, 188, 179), + QColor(39, 103, 130, 221), QColor(39, 103, 130, 96), + QColor(QStringLiteral("#F48771")), QColor(QStringLiteral("#CCA700")), + QColor(QStringLiteral("#72C892")), QColor(QStringLiteral("#75BEFF")), + QColor(QStringLiteral("#BBBEBF")), QColor(QStringLiteral("#242526")), + QColor(140, 140, 140, 77), QColor(QStringLiteral("#404040")), + QColor(QStringLiteral("#707070")), QColor(QStringLiteral("#297AA0")), + QColor(QStringLiteral("#F48771")), QColor(QStringLiteral("#8B949E")), + QColor(QStringLiteral("#A5D6FF")), QColor(QStringLiteral("#B5CEA8")), + QColor(QStringLiteral("#FF7B72")), QColor(QStringLiteral("#C586C0")), + QColor(QStringLiteral("#D2A8FF")), QColor(QStringLiteral("#4EC9B0")), + QColor(QStringLiteral("#FFA657")), QColor(QStringLiteral("#79C0FF")), + QColor(QStringLiteral("#7EE787")), QColor(QStringLiteral("#9CDCFE")), + QColor(QStringLiteral("#72C892")), QColor(QStringLiteral("#0078D4")), + QColor(QStringLiteral("#F28772")), QColor(52, 125, 57, 38), + QColor(201, 60, 55, 38) + }; +} + +AppearanceTokens AppearanceManager::lightTokens() +{ + return { + QColor(QStringLiteral("#FFFFFF")), QColor(QStringLiteral("#F0F0F0")), + QColor(QStringLiteral("#FFFFFF")), QColor(QStringLiteral("#E5F3FF")), + QColor(QStringLiteral("#A0A0A0")), QColor(QStringLiteral("#000000")), + QColor(QStringLiteral("#000000")), QColor(QStringLiteral("#555555")), + QColor(QStringLiteral("#999999")), QColor(QStringLiteral("#0078D4")), + QColor(QStringLiteral("#106EBE")), QColor(QStringLiteral("#0078D4")), + 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("#A0A0A0")), QColor(QStringLiteral("#C0C0C0")), + QColor(QStringLiteral("#707070")), QColor(QStringLiteral("#0078D4")), + QColor(QStringLiteral("#A1260D")), QColor(QStringLiteral("#008000")), + QColor(QStringLiteral("#A31515")), QColor(QStringLiteral("#098658")), + QColor(QStringLiteral("#0000FF")), QColor(QStringLiteral("#AF00DB")), + QColor(QStringLiteral("#795E26")), QColor(QStringLiteral("#267F99")), + QColor(QStringLiteral("#001080")), QColor(QStringLiteral("#0070C1")), + QColor(QStringLiteral("#800000")), QColor(QStringLiteral("#E50000")), + QColor(QStringLiteral("#2F9E44")), QColor(QStringLiteral("#0078D4")), + QColor(QStringLiteral("#E03131")), QColor(47, 158, 68, 38), + QColor(224, 49, 49, 38) + }; +} + +AppearanceTokens AppearanceManager::systemTokens(const QPalette &palette, + EffectiveAppearance effectiveAppearance, + bool highContrast) +{ + AppearanceTokens tokens = effectiveAppearance == EffectiveAppearance::Dark + ? darkTokens() + : lightTokens(); + const QColor text = palette.color(QPalette::Text); + const QColor highlight = palette.color(QPalette::Highlight); + tokens.surfaceEditor = palette.color(QPalette::Base); + tokens.surfaceShell = palette.color(QPalette::Window); + tokens.surfaceRaised = palette.color(QPalette::Button); + tokens.surfaceHover = palette.color(QPalette::AlternateBase); + tokens.borderDefault = palette.color(QPalette::Mid); + tokens.textPrimary = palette.color(QPalette::WindowText); + tokens.textEditor = text; + tokens.textSecondary = palette.color(QPalette::PlaceholderText); + tokens.textDisabled = palette.color(QPalette::Disabled, QPalette::Text); + tokens.accentPrimary = highlight; + tokens.accentHover = highlight; + tokens.focusBorder = highlight; + tokens.selectionActive = highlight; + tokens.selectionInactive = palette.color(QPalette::Inactive, QPalette::Highlight); + tokens.editorCaret = text; + tokens.editorCurrentLine = palette.color(QPalette::AlternateBase); + tokens.editorWhitespace = palette.color(QPalette::PlaceholderText); + tokens.editorIndentGuide = palette.color(QPalette::Mid); + tokens.editorIndentGuideActive = palette.color(QPalette::Dark); + tokens.editorBraceMatch = highlight; + tokens.editorBraceError = palette.color(QPalette::BrightText); + + if (highContrast) { + tokens.stateError = palette.color(QPalette::BrightText); + tokens.stateWarning = palette.color(QPalette::LinkVisited); + tokens.stateSuccess = palette.color(QPalette::Link); + tokens.stateInformation = palette.color(QPalette::Link); + tokens.syntaxComment = text; + tokens.syntaxString = text; + tokens.syntaxNumber = text; + tokens.syntaxKeyword = text; + tokens.syntaxControlFlow = text; + tokens.syntaxFunction = text; + tokens.syntaxType = text; + tokens.syntaxVariable = text; + tokens.syntaxConstant = text; + tokens.syntaxTag = text; + tokens.syntaxAttribute = text; + tokens.diffAddedMarker = palette.color(QPalette::Link); + tokens.diffModifiedMarker = highlight; + tokens.diffDeletedMarker = palette.color(QPalette::BrightText); + tokens.diffAddedFill = palette.color(QPalette::Link); + tokens.diffDeletedFill = palette.color(QPalette::BrightText); + } + + return tokens; +} + +QPalette AppearanceManager::explicitPalette(const AppearanceTokens &tokens) +{ + QPalette palette; + palette.setColor(QPalette::Window, tokens.surfaceShell); + palette.setColor(QPalette::WindowText, tokens.textPrimary); + palette.setColor(QPalette::Base, tokens.surfaceEditor); + palette.setColor(QPalette::AlternateBase, tokens.surfaceHover); + palette.setColor(QPalette::Text, tokens.textEditor); + palette.setColor(QPalette::Button, tokens.surfaceRaised); + palette.setColor(QPalette::ButtonText, tokens.textPrimary); + palette.setColor(QPalette::ToolTipBase, tokens.surfaceRaised); + palette.setColor(QPalette::ToolTipText, tokens.textPrimary); + palette.setColor(QPalette::Highlight, tokens.accentPrimary); + palette.setColor(QPalette::HighlightedText, tokens.textPrimary); + palette.setColor(QPalette::Link, tokens.accentHover); + palette.setColor(QPalette::LinkVisited, tokens.syntaxControlFlow); + palette.setColor(QPalette::PlaceholderText, tokens.textSecondary); + palette.setColor(QPalette::Light, tokens.surfaceHover); + palette.setColor(QPalette::Midlight, tokens.surfaceRaised); + palette.setColor(QPalette::Mid, tokens.borderDefault); + palette.setColor(QPalette::Dark, tokens.surfaceEditor); + palette.setColor(QPalette::Shadow, tokens.surfaceEditor); + palette.setColor(QPalette::BrightText, tokens.stateError); + palette.setColor(QPalette::Disabled, QPalette::WindowText, tokens.textDisabled); + palette.setColor(QPalette::Disabled, QPalette::Text, tokens.textDisabled); + palette.setColor(QPalette::Disabled, QPalette::ButtonText, tokens.textDisabled); + palette.setColor(QPalette::Disabled, QPalette::Highlight, tokens.selectionInactive); + return palette; +} diff --git a/src/AppearanceManager.h b/src/AppearanceManager.h new file mode 100644 index 0000000..6b9aa92 --- /dev/null +++ b/src/AppearanceManager.h @@ -0,0 +1,131 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +class ApplicationSettings; +class QWidget; + +struct AppearanceTokens +{ + QColor surfaceEditor; + QColor surfaceShell; + QColor surfaceRaised; + QColor surfaceHover; + QColor borderDefault; + QColor textPrimary; + QColor textEditor; + QColor textSecondary; + QColor textDisabled; + QColor accentPrimary; + QColor accentHover; + QColor focusBorder; + QColor selectionActive; + QColor selectionInactive; + QColor stateError; + QColor stateWarning; + QColor stateSuccess; + QColor stateInformation; + QColor editorCaret; + QColor editorCurrentLine; + QColor editorWhitespace; + QColor editorIndentGuide; + QColor editorIndentGuideActive; + QColor editorBraceMatch; + QColor editorBraceError; + QColor syntaxComment; + QColor syntaxString; + QColor syntaxNumber; + QColor syntaxKeyword; + QColor syntaxControlFlow; + QColor syntaxFunction; + QColor syntaxType; + QColor syntaxVariable; + QColor syntaxConstant; + QColor syntaxTag; + QColor syntaxAttribute; + QColor diffAddedMarker; + QColor diffModifiedMarker; + QColor diffDeletedMarker; + QColor diffAddedFill; + QColor diffDeletedFill; +}; + +class AppearanceManager : public QObject +{ + Q_OBJECT + +public: + enum class Mode { + System, + Light, + Dark + }; + Q_ENUM(Mode) + + enum class EffectiveAppearance { + Light, + Dark + }; + Q_ENUM(EffectiveAppearance) + + using ColorSchemeProvider = std::function; + + explicit AppearanceManager(ApplicationSettings *settings, QObject *parent = nullptr); + AppearanceManager(ApplicationSettings *settings, ColorSchemeProvider colorSchemeProvider, + QObject *parent = nullptr); + + Mode requestedMode() const { return mode; } + EffectiveAppearance effectiveAppearance() const { return effective; } + bool isDark() const { return effective == EffectiveAppearance::Dark; } + const AppearanceTokens &tokens() const { return currentTokens; } + + static Mode modeFromString(const QString &value); + static QString modeToString(Mode mode); + static EffectiveAppearance resolve(Mode mode, Qt::ColorScheme systemScheme, + EffectiveAppearance unknownFallback); + static int scintillaColor(const QColor &color); + static unsigned int scintillaElementColor(const QColor &color); + +public slots: + void setRequestedMode(Mode requestedMode); + void refreshSystemAppearance(); + +signals: + void effectiveAppearanceChanged(AppearanceManager::EffectiveAppearance appearance); + +protected: + bool eventFilter(QObject *watched, QEvent *event) override; + +private: + void onAppearanceSettingChanged(const QString &value); + EffectiveAppearance resolveEffectiveAppearance() const; + EffectiveAppearance paletteFallback() const; + void restoreSystemPalette(); + void applyApplicationAppearance(); + void applyNativeAppearance(); + void applyNativeAppearance(QWidget *window); + void updateEffectiveAppearance(bool forceUpdate = false); + + static AppearanceTokens darkTokens(); + static AppearanceTokens lightTokens(); + static AppearanceTokens systemTokens(const QPalette &palette, + EffectiveAppearance effectiveAppearance, + bool highContrast); + static QPalette explicitPalette(const AppearanceTokens &tokens); + + ApplicationSettings *settings; + ColorSchemeProvider colorSchemeProvider; + Mode mode; + EffectiveAppearance effective; + AppearanceTokens currentTokens; + QPalette systemPaletteSnapshot; + bool hasSystemPaletteSnapshot = false; + bool applyingPalette = false; + bool updatingAppearance = false; +}; diff --git a/src/AppearanceTrace.h b/src/AppearanceTrace.h new file mode 100644 index 0000000..381202b --- /dev/null +++ b/src/AppearanceTrace.h @@ -0,0 +1,152 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace AppearanceTrace { + +struct State +{ + QMutex mutex; + quint64 nextCycle = 0; + quint64 activeCycle = 0; + QStringList records; +}; + +inline State &state() +{ + static State value; + return value; +} + +inline QString outputPath() +{ + return qEnvironmentVariable("NOTEPADSHARP_APPEARANCE_TRACE"); +} + +inline bool enabled() +{ + return !qEnvironmentVariableIsEmpty("NOTEPADSHARP_APPEARANCE_TRACE"); +} + +inline void initialize() +{ + if (!enabled()) + return; + + const QString path = outputPath(); + QFileInfo(path).dir().mkpath(QStringLiteral(".")); + QFile file(path); + if (file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) { + file.write(QStringLiteral("%1 pid=%2 trace-enabled version=%3\n") + .arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs)) + .arg(QCoreApplication::applicationPid()) + .arg(QCoreApplication::applicationVersion()).toUtf8()); + file.flush(); + } +} + +inline quint64 beginCycle(const QString &trigger, const QString &details) +{ + if (!enabled()) + return 0; + + State &trace = state(); + QMutexLocker locker(&trace.mutex); + trace.activeCycle = ++trace.nextCycle; + trace.records = { + QStringLiteral("%1 pid=%2 cycle=%3 begin trigger=%4 %5") + .arg(QDateTime::currentDateTime().toString(Qt::ISODateWithMs)) + .arg(QCoreApplication::applicationPid()) + .arg(trace.activeCycle) + .arg(trigger, details) + }; + return trace.activeCycle; +} + +inline quint64 currentCycle() +{ + State &trace = state(); + QMutexLocker locker(&trace.mutex); + return trace.activeCycle; +} + +inline void record(quint64 cycle, const QString &component, qint64 elapsedMs, + const QString &details = {}) +{ + if (cycle == 0) + return; + + State &trace = state(); + QMutexLocker locker(&trace.mutex); + if (trace.activeCycle != cycle) + return; + + trace.records.append(QStringLiteral("cycle=%1 component=%2 elapsed-ms=%3 %4") + .arg(cycle).arg(component).arg(elapsedMs).arg(details)); +} + +inline void endCycle(quint64 cycle, qint64 elapsedMs, const QString &details) +{ + if (cycle == 0) + return; + + QStringList records; + { + State &trace = state(); + QMutexLocker locker(&trace.mutex); + if (trace.activeCycle != cycle) + return; + + trace.records.append(QStringLiteral("cycle=%1 end elapsed-ms=%2 %3") + .arg(cycle).arg(elapsedMs).arg(details)); + records = trace.records; + trace.records.clear(); + trace.activeCycle = 0; + } + + const QString path = outputPath(); + QFileInfo(path).dir().mkpath(QStringLiteral(".")); + QFile file(path); + if (file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) { + file.write(records.join(QLatin1Char('\n')).toUtf8()); + file.write("\n"); + file.flush(); + } +} + +class Scope +{ +public: + Scope(QString component, QString details = {}) + : cycle(enabled() ? currentCycle() : 0) + , component(std::move(component)) + , details(std::move(details)) + { + if (cycle != 0) + timer.start(); + } + + ~Scope() + { + if (cycle != 0) + record(cycle, component, timer.elapsed(), details); + } + +private: + quint64 cycle; + QString component; + QString details; + QElapsedTimer timer; +}; + +} // namespace AppearanceTrace \ No newline at end of file diff --git a/src/ApplicationSettings.cpp b/src/ApplicationSettings.cpp index 2193f5f..6c6579f 100644 --- a/src/ApplicationSettings.cpp +++ b/src/ApplicationSettings.cpp @@ -40,6 +40,11 @@ ApplicationSettings::ApplicationSettings(QObject *parent) { } +ApplicationSettings::ApplicationSettings(const QString &fileName, QSettings::Format format, QObject *parent) + : QSettings{fileName, format, parent} +{ +} + CREATE_SETTING(Gui, ShowMenuBar, showMenuBar, bool, true) CREATE_SETTING(Gui, ShowToolBar, showToolBar, bool, true) CREATE_SETTING(Gui, ShowTabBar, showTabBar, bool, true) @@ -60,6 +65,8 @@ CREATE_SETTING(App, DefaultDirectory, defaultDirectory, QString, QString()) CREATE_SETTING(App, Translation, translation, QString, QStringLiteral("")) +CREATE_SETTING(Gui, Appearance, appearance, QString, QStringLiteral("light")) + CREATE_SETTING(Editor, ShowWhitespace, showWhitespace, bool, false); CREATE_SETTING(Editor, ShowEndOfLine, showEndOfLine, bool, false); CREATE_SETTING(Editor, ShowWrapSymbol, showWrapSymbol, bool, false); diff --git a/src/ApplicationSettings.h b/src/ApplicationSettings.h index 481475e..a897bc5 100644 --- a/src/ApplicationSettings.h +++ b/src/ApplicationSettings.h @@ -68,6 +68,7 @@ class ApplicationSettings : public QSettings public: explicit ApplicationSettings(QObject *parent = nullptr); + ApplicationSettings(const QString &fileName, QSettings::Format format, QObject *parent = nullptr); enum DefaultDirectoryBehaviorEnum { FollowCurrentDocument, @@ -108,6 +109,8 @@ class ApplicationSettings : public QSettings DEFINE_SETTING(Translation, translation, QString) + DEFINE_SETTING(Appearance, appearance, QString) + DEFINE_SETTING(ShowWhitespace, showWhitespace, bool); DEFINE_SETTING(ShowEndOfLine, showEndOfLine, bool); DEFINE_SETTING(ShowWrapSymbol, showWrapSymbol, bool) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b956254..8f184f2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,6 +2,7 @@ qt_add_executable(NotepadSharp WIN32 MACOSX_BUNDLE ActionUtils.h + AppearanceManager.h ApplicationSettings.h ByteArrayUtils.h ColorPickerDelegate.h @@ -17,6 +18,7 @@ qt_add_executable(NotepadSharp DefaultDirectoryManager.h DockedEditor.h DockedEditorTitleBar.h + EditorAppearance.h EditorManager.h EditorPrintPreviewRenderer.h @@ -55,9 +57,11 @@ qt_add_executable(NotepadSharp SpinBoxDelegate.h TranslationManager.h UndoAction.h + WindowsAppearance.h ZoomEventWatcher.h resources.qrc scripts.qrc + AppearanceManager.cpp ApplicationSettings.cpp ColorPickerDelegate.cpp ComboBoxDelegate.cpp @@ -70,6 +74,8 @@ qt_add_executable(NotepadSharp DebugManager.cpp DefaultDirectoryManager.cpp DockedEditor.cpp + EditorDecorationAppearance.cpp + EditorAppearance.cpp EditorManager.cpp EditorPrintPreviewRenderer.cpp @@ -105,6 +111,7 @@ qt_add_executable(NotepadSharp SpinBoxDelegate.cpp TranslationManager.cpp UndoAction.cpp + WindowsAppearance.cpp ZoomEventWatcher.cpp decorators/ApplicationDecorator.cpp decorators/ApplicationDecorator.h @@ -230,6 +237,7 @@ if(WIN32) @ONLY ) target_sources(NotepadSharp PRIVATE "${CMAKE_BINARY_DIR}/version.rc") + target_link_libraries(NotepadSharp PRIVATE dwmapi) configure_file( "${CMAKE_SOURCE_DIR}/icon/NotepadSharp.ico" "${CMAKE_BINARY_DIR}/NotepadSharp.ico" diff --git a/src/EditorAppearance.cpp b/src/EditorAppearance.cpp new file mode 100644 index 0000000..9a1776d --- /dev/null +++ b/src/EditorAppearance.cpp @@ -0,0 +1,68 @@ +#include "EditorAppearance.h" + +#include "AppearanceManager.h" + +#include "Scintilla.h" +#include "ScintillaEdit.h" + +namespace { + +constexpr int HiddenLinesUnderlineMarker = 21; + +} + +void EditorAppearance::apply(ScintillaEdit *editor, const AppearanceTokens &tokens, + const QString &fontName, int fontSize) +{ + editor->styleSetFore(STYLE_DEFAULT, AppearanceManager::scintillaColor(tokens.textEditor)); + editor->styleSetBack(STYLE_DEFAULT, AppearanceManager::scintillaColor(tokens.surfaceEditor)); + editor->styleSetSize(STYLE_DEFAULT, fontSize); + editor->styleSetFont(STYLE_DEFAULT, fontName.toUtf8().data()); + editor->styleClearAll(); + + for (int marker = SC_MARKNUM_FOLDEREND; marker <= SC_MARKNUM_FOLDEROPEN; ++marker) { + editor->markerSetFore(marker, AppearanceManager::scintillaColor(tokens.surfaceEditor)); + editor->markerSetBack(marker, AppearanceManager::scintillaColor(tokens.textSecondary)); + editor->markerSetBackSelected(marker, AppearanceManager::scintillaColor(tokens.accentPrimary)); + } + + editor->markerSetBack(HiddenLinesUnderlineMarker, + AppearanceManager::scintillaColor(tokens.stateSuccess)); + editor->setEdgeColour(AppearanceManager::scintillaColor(tokens.borderDefault)); + editor->setElementColour(SC_ELEMENT_SELECTION_BACK, + AppearanceManager::scintillaElementColor(tokens.selectionActive)); + editor->setElementColour(SC_ELEMENT_SELECTION_INACTIVE_BACK, + AppearanceManager::scintillaElementColor(tokens.selectionInactive)); + editor->setElementColour(SC_ELEMENT_CARET_LINE_BACK, + AppearanceManager::scintillaElementColor(tokens.editorCurrentLine)); + editor->setElementColour(SC_ELEMENT_WHITE_SPACE, + AppearanceManager::scintillaElementColor(tokens.editorWhitespace)); + editor->setElementColour(SC_ELEMENT_FOLD_LINE, + AppearanceManager::scintillaElementColor(tokens.editorIndentGuide)); + editor->setFoldMarginColour(true, AppearanceManager::scintillaColor(tokens.surfaceShell)); + editor->setFoldMarginHiColour(true, AppearanceManager::scintillaColor(tokens.surfaceRaised)); + editor->setCaretFore(AppearanceManager::scintillaColor(tokens.editorCaret)); + + applyNamedStyles(editor, tokens); +} + +void EditorAppearance::applyNamedStyles(ScintillaEdit *editor, const AppearanceTokens &tokens) +{ + editor->styleSetFore(STYLE_LINENUMBER, + AppearanceManager::scintillaColor(tokens.textSecondary)); + editor->styleSetBack(STYLE_LINENUMBER, + AppearanceManager::scintillaColor(tokens.surfaceShell)); + editor->styleSetBold(STYLE_LINENUMBER, false); + editor->styleSetFore(STYLE_BRACELIGHT, + AppearanceManager::scintillaColor(tokens.textPrimary)); + editor->styleSetBack(STYLE_BRACELIGHT, + AppearanceManager::scintillaColor(tokens.editorBraceMatch)); + editor->styleSetFore(STYLE_BRACEBAD, + AppearanceManager::scintillaColor(tokens.textPrimary)); + editor->styleSetBack(STYLE_BRACEBAD, + AppearanceManager::scintillaColor(tokens.editorBraceError)); + editor->styleSetFore(STYLE_INDENTGUIDE, + AppearanceManager::scintillaColor(tokens.editorIndentGuide)); + editor->styleSetBack(STYLE_INDENTGUIDE, + AppearanceManager::scintillaColor(tokens.surfaceEditor)); +} diff --git a/src/EditorAppearance.h b/src/EditorAppearance.h new file mode 100644 index 0000000..6bb06da --- /dev/null +++ b/src/EditorAppearance.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +struct AppearanceTokens; +class ScintillaEdit; +class ScintillaNext; + +namespace EditorAppearance { + +void apply(ScintillaEdit *editor, const AppearanceTokens &tokens, + const QString &fontName, int fontSize); +void applyNamedStyles(ScintillaEdit *editor, const AppearanceTokens &tokens); +void applyDecorations(ScintillaNext *editor, const AppearanceTokens &tokens); + +} diff --git a/src/EditorDecorationAppearance.cpp b/src/EditorDecorationAppearance.cpp new file mode 100644 index 0000000..168b435 --- /dev/null +++ b/src/EditorDecorationAppearance.cpp @@ -0,0 +1,36 @@ +#include "EditorAppearance.h" + +#include "AppearanceManager.h" +#include "ScintillaNext.h" + +void EditorAppearance::applyDecorations(ScintillaNext *editor, const AppearanceTokens &tokens) +{ + editor->markerSetFore(24, AppearanceManager::scintillaColor(tokens.diffModifiedMarker)); + editor->markerSetBack(24, AppearanceManager::scintillaColor(tokens.diffModifiedMarker)); + + const int braceHighlight = editor->allocateIndicator("brace_highlight"); + const int braceBadlight = editor->allocateIndicator("brace_badlight"); + const int smartHighlighter = editor->allocateIndicator("smart_highlighter"); + const int urlFinder = editor->allocateIndicator("url_finder"); + editor->indicSetFore(braceHighlight, + AppearanceManager::scintillaColor(tokens.editorBraceMatch)); + editor->indicSetFore(braceBadlight, + AppearanceManager::scintillaColor(tokens.editorBraceError)); + editor->indicSetFore(smartHighlighter, + AppearanceManager::scintillaColor(tokens.accentHover)); + editor->indicSetFore(urlFinder, AppearanceManager::scintillaColor(tokens.accentHover)); + editor->indicSetHoverFore(urlFinder, + AppearanceManager::scintillaColor(tokens.accentPrimary)); + + const QColor markerColors[] = { + tokens.syntaxString, + tokens.syntaxVariable, + tokens.syntaxTag, + }; + for (int index = 0; index < 3; ++index) { + const int indicator = editor->allocateIndicator( + QStringLiteral("marker_%1").arg(index)); + editor->indicSetFore(indicator, + AppearanceManager::scintillaColor(markerColors[index])); + } +} diff --git a/src/EditorManager.cpp b/src/EditorManager.cpp index 7a0220b..3fff28c 100644 --- a/src/EditorManager.cpp +++ b/src/EditorManager.cpp @@ -19,6 +19,8 @@ #include #include "ApplicationSettings.h" +#include "AppearanceManager.h" +#include "EditorAppearance.h" #include "EditorManager.h" #include "ScintillaNext.h" @@ -43,8 +45,9 @@ const int MARK_HIDELINESEND = 22; const int MARK_HIDELINESUNDERLINE = 21; -EditorManager::EditorManager(ApplicationSettings *settings, QObject *parent) - : QObject(parent), settings(settings) +EditorManager::EditorManager(ApplicationSettings *settings, AppearanceManager *appearanceManager, + QObject *parent) + : QObject(parent), settings(settings), appearanceManager(appearanceManager) { connect(this, &EditorManager::editorCreated, this, [=](ScintillaNext *editor) { connect(editor, &ScintillaNext::closed, this, [=]() { @@ -193,11 +196,6 @@ void EditorManager::setupEditor(ScintillaNext *editor) editor->clearCmdKey(SCK_INSERT); editor->setFoldMarkers(QStringLiteral("box")); - for (int i = SC_MARKNUM_FOLDEREND; i <= SC_MARKNUM_FOLDEROPEN; ++i) { - editor->markerSetFore(i, 0xF3F3F3); - editor->markerSetBack(i, 0x808080); - editor->markerSetBackSelected(i, 0x0000FF); - } editor->setIdleStyling(SC_IDLESTYLING_TOVISIBLE); editor->setEndAtLastLine(false); @@ -215,7 +213,6 @@ void EditorManager::setupEditor(ScintillaNext *editor) editor->setMarginWidthN(2, 14); editor->markerDefine(MARK_HIDELINESUNDERLINE, SC_MARK_UNDERLINE); - editor->markerSetBack(MARK_HIDELINESUNDERLINE, 0x77CC77); editor->markerDefine(MARK_HIDELINESBEGIN, SC_MARK_ARROW); editor->markerDefine(MARK_HIDELINESEND, SC_MARK_ARROWDOWN); @@ -234,56 +231,15 @@ void EditorManager::setupEditor(ScintillaNext *editor) editor->setCaretLineVisibleAlways(true); editor->setCaretWidth(2); - editor->setEdgeColour(0x80FFFF); - - // https://www.scintilla.org/ScintillaDoc.html#ElementColours - // SC_ELEMENT_SELECTION_TEXT - // SC_ELEMENT_SELECTION_BACK - // SC_ELEMENT_SELECTION_ADDITIONAL_TEXT - // SC_ELEMENT_SELECTION_ADDITIONAL_BACK - // SC_ELEMENT_SELECTION_SECONDARY_TEXT - // SC_ELEMENT_SELECTION_SECONDARY_BACK - // SC_ELEMENT_SELECTION_INACTIVE_TEXT - editor->setElementColour(SC_ELEMENT_SELECTION_INACTIVE_BACK, 0xFFE0E0E0); - // SC_ELEMENT_CARET - // SC_ELEMENT_CARET_ADDITIONAL - editor->setElementColour(SC_ELEMENT_CARET_LINE_BACK, 0xFFFFE8E8); - editor->setElementColour(SC_ELEMENT_WHITE_SPACE, 0xFFD0D0D0); - // SC_ELEMENT_WHITE_SPACE_BACK - // SC_ELEMENT_HOT_SPOT_ACTIVE - // SC_ELEMENT_HOT_SPOT_ACTIVE_BACK - editor->setElementColour(SC_ELEMENT_FOLD_LINE, 0xFFA0A0A0); - // SC_ELEMENT_HIDDEN_LINE - editor->setWhitespaceSize(2); - editor->setFoldMarginColour(true, 0xFFFFFF); - editor->setFoldMarginHiColour(true, 0xE9E9E9); - editor->setAutomaticFold(SC_AUTOMATICFOLD_SHOW | SC_AUTOMATICFOLD_CLICK | SC_AUTOMATICFOLD_CHANGE); editor->markerEnableHighlight(true); editor->setCharsDefault(); editor->setWordChars(editor->wordChars() + settings->additionalWordChars().toLatin1()); - editor->styleSetFore(STYLE_DEFAULT, 0x000000); - editor->styleSetBack(STYLE_DEFAULT, 0xFFFFFF); - editor->styleSetSize(STYLE_DEFAULT, settings->fontSize()); - editor->styleSetFont(STYLE_DEFAULT, settings->fontName().toUtf8().data()); - editor->styleClearAll(); - - editor->styleSetFore(STYLE_LINENUMBER, 0x808080); - editor->styleSetBack(STYLE_LINENUMBER, 0xE4E4E4); - editor->styleSetBold(STYLE_LINENUMBER, false); - - editor->styleSetFore(STYLE_BRACELIGHT, 0x0000FF); - editor->styleSetBack(STYLE_BRACELIGHT, 0xFFFFFF); - - editor->styleSetFore(STYLE_BRACEBAD, 0x000080); - editor->styleSetBack(STYLE_BRACEBAD, 0xFFFFFF); - - editor->styleSetFore(STYLE_INDENTGUIDE, 0xC0C0C0); - editor->styleSetBack(STYLE_INDENTGUIDE, 0xFFFFFF); + applyEditorTheme(editor); // STYLE_CONTROLCHAR // STYLE_CALLTIP @@ -315,7 +271,8 @@ void EditorManager::setupEditor(ScintillaNext *editor) SmartHighlighter *s = new SmartHighlighter(editor); s->setEnabled(true); - HighlightedScrollBarDecorator *h = new HighlightedScrollBarDecorator(editor); + HighlightedScrollBarDecorator *h = new HighlightedScrollBarDecorator( + editor, appearanceManager); h->setEnabled(true); BraceMatch *b = new BraceMatch(editor); @@ -343,6 +300,27 @@ void EditorManager::setupEditor(ScintillaNext *editor) bm->setEnabled(true); new HTMLAutoCompleteDecorator(editor); + EditorAppearance::applyDecorations(editor, appearanceManager->tokens()); +} + +void EditorManager::applyEditorTheme(ScintillaNext *editor) +{ + EditorAppearance::apply(editor, appearanceManager->tokens(), + settings->fontName(), settings->fontSize()); + EditorAppearance::applyDecorations(editor, appearanceManager->tokens()); + if (editor->verticalScrollBar()) + editor->verticalScrollBar()->update(); +} + +void EditorManager::applyEditorNamedStyles(ScintillaNext *editor) +{ + EditorAppearance::applyNamedStyles(editor, appearanceManager->tokens()); +} + +void EditorManager::applyAppearanceToAllEditors() +{ + for (ScintillaNext *editor : getEditors()) + applyEditorTheme(editor); } void EditorManager::purgeOldEditorPointers() diff --git a/src/EditorManager.h b/src/EditorManager.h index 0aebb1e..acfb294 100644 --- a/src/EditorManager.h +++ b/src/EditorManager.h @@ -25,6 +25,7 @@ class ApplicationSettings; +class AppearanceManager; class ScintillaNext; class EditorManager : public QObject @@ -32,7 +33,8 @@ class EditorManager : public QObject Q_OBJECT public: - explicit EditorManager(ApplicationSettings *settings, QObject *parent = nullptr); + explicit EditorManager(ApplicationSettings *settings, AppearanceManager *appearanceManager, + QObject *parent = nullptr); ScintillaNext *createEditor(const QString &name); ScintillaNext *createEditorFromFile(const QString &filePath, bool tryToCreate=false); @@ -40,6 +42,10 @@ class EditorManager : public QObject ScintillaNext *getEditorByFilePath(const QString &filePath); void manageEditor(ScintillaNext *editor); + void applyEditorTheme(ScintillaNext *editor); + void applyEditorNamedStyles(ScintillaNext *editor); + void applyAppearanceToAllEditors(); + QList> getEditors(); signals: void editorCreated(ScintillaNext *editor); @@ -48,11 +54,11 @@ class EditorManager : public QObject private: void setupEditor(ScintillaNext *editor); void purgeOldEditorPointers(); - QList> getEditors(); int detectEOLMode(ScintillaNext *editor) const; QList> editors; ApplicationSettings *settings; + AppearanceManager *appearanceManager; }; #endif // EDITORMANAGER_H diff --git a/src/LuaState.cpp b/src/LuaState.cpp index 3a7b8a2..6585e15 100644 --- a/src/LuaState.cpp +++ b/src/LuaState.cpp @@ -133,6 +133,12 @@ void LuaState::setVariable(const char *name, bool value) lua_setglobal(L, name); } +void LuaState::setVariable(const char *name, int value) +{ + lua_pushinteger(L, static_cast(value)); + lua_setglobal(L, name); +} + void LuaState::internal_execute(const char *statement, bool clear) { // There may be other things on the stack so save the top of it diff --git a/src/LuaState.h b/src/LuaState.h index 61aaf73..6d62a34 100644 --- a/src/LuaState.h +++ b/src/LuaState.h @@ -36,6 +36,7 @@ class LuaState void setVariable(const char *name, const QString &value); void setVariable(const char *name, const char *value); void setVariable(const char *name, bool value); + void setVariable(const char *name, int value); void executeFile(const QString &fileName); void clearStack(); diff --git a/src/NotepadSharpApplication.cpp b/src/NotepadSharpApplication.cpp index 937be8a..80fec4f 100644 --- a/src/NotepadSharpApplication.cpp +++ b/src/NotepadSharpApplication.cpp @@ -27,6 +27,8 @@ #include "SessionManager.h" #include "TranslationManager.h" #include "ApplicationSettings.h" +#include "AppearanceManager.h" +#include "AppearanceTrace.h" #include "LuaState.h" #include "lua.hpp" @@ -105,6 +107,8 @@ bool NotepadSharpApplication::init() settings->clear(); } + appearanceManager = new AppearanceManager(settings, this); + // Translation files are stored as a qresource translationManager = new TranslationManager(this, QStringLiteral(":/i18n/")); @@ -125,7 +129,7 @@ bool NotepadSharpApplication::init() luaState = new LuaState(); recentFilesListManager = new RecentFilesListManager(this); - editorManager = new EditorManager(settings, this); + editorManager = new EditorManager(settings, appearanceManager, this); sessionManager = new SessionManager(this); connect(editorManager, &EditorManager::editorCreated, recentFilesListManager, [this](ScintillaNext *editor) { @@ -149,9 +153,13 @@ bool NotepadSharpApplication::init() MarkerAppDecorator *mad = new MarkerAppDecorator(this); mad->setEnabled(true); - luaState->executeFile(":/scripts/init.lua"); + updateLuaAppearance(); + luaState->executeFile(":/scripts/init.lua"); LuaExtension::Instance().Initialise(luaState->L, Q_NULLPTR); + connect(appearanceManager, &AppearanceManager::effectiveAppearanceChanged, + this, &NotepadSharpApplication::refreshEditorAppearance); + createNewWindow(); connect(editorManager, &EditorManager::editorCreated, window, &MainWindow::addEditor); @@ -295,6 +303,55 @@ void NotepadSharpApplication::setEditorLanguage(ScintillaNext *editor, const QSt getLuaState()->setVariable("skip_tabwidth", skipTabWidth); getLuaState()->execute("SetLanguage(languageName)"); + editorManager->applyEditorNamedStyles(editor); +} + +void NotepadSharpApplication::updateLuaAppearance() const +{ + const AppearanceTokens &tokens = appearanceManager->tokens(); + luaState->setVariable("theme_dark_mode", appearanceManager->isDark()); + luaState->setVariable("theme_default_fg", AppearanceManager::scintillaColor(tokens.textEditor)); + luaState->setVariable("theme_default_bg", AppearanceManager::scintillaColor(tokens.surfaceEditor)); + luaState->setVariable("theme_comment", AppearanceManager::scintillaColor(tokens.syntaxComment)); + luaState->setVariable("theme_string", AppearanceManager::scintillaColor(tokens.syntaxString)); + luaState->setVariable("theme_number", AppearanceManager::scintillaColor(tokens.syntaxNumber)); + luaState->setVariable("theme_keyword", AppearanceManager::scintillaColor(tokens.syntaxKeyword)); + luaState->setVariable("theme_control_flow", AppearanceManager::scintillaColor(tokens.syntaxControlFlow)); + luaState->setVariable("theme_function", AppearanceManager::scintillaColor(tokens.syntaxFunction)); + luaState->setVariable("theme_type", AppearanceManager::scintillaColor(tokens.syntaxType)); + luaState->setVariable("theme_variable", AppearanceManager::scintillaColor(tokens.syntaxVariable)); + luaState->setVariable("theme_constant", AppearanceManager::scintillaColor(tokens.syntaxConstant)); + luaState->setVariable("theme_tag", AppearanceManager::scintillaColor(tokens.syntaxTag)); + luaState->setVariable("theme_attribute", AppearanceManager::scintillaColor(tokens.syntaxAttribute)); + luaState->setVariable("theme_error", AppearanceManager::scintillaColor(tokens.stateError)); +} + +void NotepadSharpApplication::refreshEditorLanguageAppearance(ScintillaNext *editor) const +{ + LuaExtension::Instance().setEditor(editor); + getLuaState()->setVariable("languageName", editor->languageName); + getLuaState()->execute("SetLanguageAppearance(languageName)"); + editorManager->applyEditorNamedStyles(editor); +} + +void NotepadSharpApplication::refreshEditorAppearance() +{ + const QList> editors = editorManager->getEditors(); + AppearanceTrace::Scope totalTrace(QStringLiteral("editor-refresh-total"), + QStringLiteral("editors=%1").arg(editors.size())); + { + AppearanceTrace::Scope luaTrace(QStringLiteral("editor-lua-theme")); + updateLuaAppearance(); + luaState->execute("UpdateTheme()"); + } + + for (ScintillaNext *editor : editors) { + editorManager->applyEditorTheme(editor); + if (editor->languageName.isEmpty()) + editorManager->applyEditorNamedStyles(editor); + else + refreshEditorLanguageAppearance(editor); + } } QStringList NotepadSharpApplication::getLanguageKeywords(const QString &languageName) const diff --git a/src/NotepadSharpApplication.h b/src/NotepadSharpApplication.h index 3025287..fd388c9 100644 --- a/src/NotepadSharpApplication.h +++ b/src/NotepadSharpApplication.h @@ -30,6 +30,7 @@ class MainWindow; +class AppearanceManager; class LuaState; class EditorManager; class RecentFilesListManager; @@ -56,6 +57,7 @@ class NotepadSharpApplication : public SingleApplication QString getFileDialogFilter() const; QString getFileDialogFilterForLanguage(const QString &language) const; ApplicationSettings *getSettings() const { return settings; } + AppearanceManager *getAppearanceManager() const { return appearanceManager; } QStringList getLanguages() const; void setEditorLanguage(ScintillaNext *editor, const QString &languageName) const; @@ -75,6 +77,7 @@ class NotepadSharpApplication : public SingleApplication bool event(QEvent *event) override; private slots: + void refreshEditorAppearance(); void saveSettings(); void receiveInfoFromSecondaryInstance(quint32 instanceId, QByteArray message); void saveSession(); @@ -83,8 +86,11 @@ private slots: void openFiles(const QStringList &files); void loadSettings(); + void updateLuaAppearance() const; + void refreshEditorLanguageAppearance(ScintillaNext *editor) const; EditorManager *editorManager; + AppearanceManager *appearanceManager; RecentFilesListManager *recentFilesListManager; ApplicationSettings *settings; SessionManager *sessionManager; diff --git a/src/SearchResultHighlighterDelegate.cpp b/src/SearchResultHighlighterDelegate.cpp index 87cad54..313c0b2 100644 --- a/src/SearchResultHighlighterDelegate.cpp +++ b/src/SearchResultHighlighterDelegate.cpp @@ -17,6 +17,7 @@ */ #include "SearchResultHighlighterDelegate.h" +#include "AppearanceManager.h" #include "SearchResultData.h" #include @@ -58,26 +59,30 @@ void SearchResultHighlighterDelegate::paint(QPainter *painter, const QStyleOptio // Draw 'before' text (normal) painter->setFont(opt.font); - painter->setPen(opt.palette.color(QPalette::Text)); + const QColor normalText = opt.state & QStyle::State_Selected + ? opt.palette.color(QPalette::HighlightedText) + : opt.palette.color(QPalette::Text); + painter->setPen(normalText); painter->drawText(x, y, before); x += fm.horizontalAdvance(before); - // Draw highlighted 'match' text (bold, red, yellow bg) QFont boldFont = opt.font; boldFont.setBold(true); painter->setFont(boldFont); int matchWidth = fm.horizontalAdvance(match); QRect highlightRect(x, textRect.top(), matchWidth, textRect.height()); - painter->fillRect(highlightRect, QColor(Qt::yellow)); + QColor matchBackground = appearanceManager->tokens().accentPrimary; + matchBackground.setAlpha(110); + painter->fillRect(highlightRect, matchBackground); - painter->setPen(Qt::red); + painter->setPen(appearanceManager->tokens().textPrimary); painter->drawText(x, y, match); x += matchWidth; // Draw 'after' text (normal) painter->setFont(opt.font); - painter->setPen(opt.palette.color(QPalette::Text)); + painter->setPen(normalText); painter->drawText(x, y, after); painter->restore(); diff --git a/src/SearchResultHighlighterDelegate.h b/src/SearchResultHighlighterDelegate.h index cbd8414..0835539 100644 --- a/src/SearchResultHighlighterDelegate.h +++ b/src/SearchResultHighlighterDelegate.h @@ -20,9 +20,15 @@ #include +class AppearanceManager; + class SearchResultHighlighterDelegate : public QStyledItemDelegate { public: - SearchResultHighlighterDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) {} + SearchResultHighlighterDelegate(AppearanceManager *appearanceManager, QObject *parent = nullptr) + : QStyledItemDelegate(parent), appearanceManager(appearanceManager) {} void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + +private: + AppearanceManager *appearanceManager; }; diff --git a/src/WindowsAppearance.cpp b/src/WindowsAppearance.cpp new file mode 100644 index 0000000..5c0f5bb --- /dev/null +++ b/src/WindowsAppearance.cpp @@ -0,0 +1,74 @@ +#include "WindowsAppearance.h" + +#include + +#ifdef Q_OS_WIN +#include +#include +#endif + +bool WindowsAppearance::isHighContrast() +{ +#ifdef Q_OS_WIN + HIGHCONTRASTW highContrast{sizeof(HIGHCONTRASTW)}; + return SystemParametersInfoW(SPI_GETHIGHCONTRAST, sizeof(highContrast), + &highContrast, 0) + && (highContrast.dwFlags & HCF_HIGHCONTRASTON); +#else + return false; +#endif +} + +void WindowsAppearance::applyToWindow(QWidget *window, bool dark, bool followSystem, + const QColor &caption, const QColor &text, + const QColor &border) +{ +#ifdef Q_OS_WIN + if (!window || !window->isWindow()) + return; + + const HWND handle = reinterpret_cast(window->winId()); + if (followSystem) { + const COLORREF defaultColor = DWMWA_COLOR_DEFAULT; + DwmSetWindowAttribute(handle, DWMWA_CAPTION_COLOR, + &defaultColor, sizeof(defaultColor)); + DwmSetWindowAttribute(handle, DWMWA_TEXT_COLOR, + &defaultColor, sizeof(defaultColor)); + DwmSetWindowAttribute(handle, DWMWA_BORDER_COLOR, + &defaultColor, sizeof(defaultColor)); + + if (isHighContrast()) { + const BOOL useDarkMode = FALSE; + DwmSetWindowAttribute(handle, DWMWA_USE_IMMERSIVE_DARK_MODE, + &useDarkMode, sizeof(useDarkMode)); + return; + } + + const BOOL useDarkMode = dark ? TRUE : FALSE; + DwmSetWindowAttribute(handle, DWMWA_USE_IMMERSIVE_DARK_MODE, + &useDarkMode, sizeof(useDarkMode)); + return; + } + + const BOOL useDarkMode = dark ? TRUE : FALSE; + DwmSetWindowAttribute(handle, DWMWA_USE_IMMERSIVE_DARK_MODE, + &useDarkMode, sizeof(useDarkMode)); + + const COLORREF captionColor = RGB(caption.red(), caption.green(), caption.blue()); + const COLORREF textColor = RGB(text.red(), text.green(), text.blue()); + const COLORREF borderColor = RGB(border.red(), border.green(), border.blue()); + DwmSetWindowAttribute(handle, DWMWA_CAPTION_COLOR, + &captionColor, sizeof(captionColor)); + DwmSetWindowAttribute(handle, DWMWA_TEXT_COLOR, + &textColor, sizeof(textColor)); + DwmSetWindowAttribute(handle, DWMWA_BORDER_COLOR, + &borderColor, sizeof(borderColor)); +#else + Q_UNUSED(window) + Q_UNUSED(dark) + Q_UNUSED(followSystem) + Q_UNUSED(caption) + Q_UNUSED(text) + Q_UNUSED(border) +#endif +} diff --git a/src/WindowsAppearance.h b/src/WindowsAppearance.h new file mode 100644 index 0000000..d647120 --- /dev/null +++ b/src/WindowsAppearance.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +class QWidget; + +namespace WindowsAppearance { + +bool isHighContrast(); +void applyToWindow(QWidget *window, bool dark, bool followSystem, + const QColor &caption, const QColor &text, const QColor &border); + +} diff --git a/src/compare/CompareOverlay.cpp b/src/compare/CompareOverlay.cpp index c853a77..72e455a 100644 --- a/src/compare/CompareOverlay.cpp +++ b/src/compare/CompareOverlay.cpp @@ -9,6 +9,7 @@ #include "CompareOverlay.h" +#include "AppearanceManager.h" #include "ScintillaNext.h" #include @@ -37,25 +38,68 @@ constexpr int CompareMarkerMask = (1 << AddedBackgroundMarker) | (1 << DeletedMarker) | (1 << CurrentMarker); -constexpr int AddedColor = 0xC7E4B7; -constexpr int DeletedColor = 0xC1C1F7; -constexpr int InlineAddedColor = 0x9CCC9C; -constexpr int InlineDeletedColor = 0x8B8BE5; -constexpr int CurrentColor = 0xE97D2E; -constexpr int CompareMarginBackground = 0xF2F2F2; -constexpr int GapBackground = 0xF4F4F4; -constexpr int GapForeground = 0xD7D7D7; constexpr auto AnnotationStyleProperty = "compareAnnotationStyle"; +struct CompareColors +{ + QColor addedMarker; + QColor deletedMarker; + QColor addedFill; + QColor deletedFill; + QColor current; + QColor currentForeground; + QColor marginBackground; + QColor gapBackground; + QColor gapForeground; +}; + +int scintillaColor(const QColor &color) +{ + return color.red() | (color.green() << 8) | (color.blue() << 16); +} + +CompareColors compareColors(AppearanceManager *appearanceManager) +{ + if (appearanceManager) { + const AppearanceTokens &tokens = appearanceManager->tokens(); + return { + tokens.diffAddedMarker, + tokens.diffDeletedMarker, + tokens.diffAddedFill, + tokens.diffDeletedFill, + tokens.accentPrimary, + tokens.textPrimary, + tokens.surfaceShell, + tokens.surfaceRaised, + tokens.borderDefault, + }; + } + + return { + QColor(QStringLiteral("#2F9E44")), + QColor(QStringLiteral("#E03131")), + QColor(47, 158, 68, 38), + QColor(224, 49, 49, 38), + QColor(QStringLiteral("#E97D2E")), + QColor(QStringLiteral("#FFFFFF")), + QColor(QStringLiteral("#F2F2F2")), + QColor(QStringLiteral("#F4F4F4")), + QColor(QStringLiteral("#D7D7D7")), + }; +} + class TopGapWidget : public QWidget { public: - TopGapWidget(ScintillaNext *editor, QMargins baseMargins, int lineCount, int lineHeight) : + TopGapWidget(ScintillaNext *editor, QMargins baseMargins, int lineCount, int lineHeight, + QColor background, QColor foreground) : QWidget(editor), editor(editor), baseMargins(baseMargins), lineCount(lineCount), - lineHeight(lineHeight) + lineHeight(lineHeight), + background(background), + foreground(foreground) { setAttribute(Qt::WA_TransparentForMouseEvents); setProperty("compareGapLineCount", lineCount); @@ -94,12 +138,14 @@ class TopGapWidget : public QWidget void paintEvent(QPaintEvent *) override { QPainter painter(this); - painter.fillRect(rect(), QColor(244, 244, 244)); - painter.setPen(QColor(215, 215, 215)); + painter.fillRect(rect(), background); + painter.setPen(foreground); for (int x = -height(); x < width(); x += 8) { painter.drawLine(x, height(), x + height(), 0); } - painter.setPen(QColor(232, 232, 232)); + QColor separator = foreground; + separator.setAlpha(110); + painter.setPen(separator); for (int line = 1; line < lineCount; ++line) { painter.drawLine(0, line * lineHeight, width(), line * lineHeight); } @@ -123,6 +169,8 @@ class TopGapWidget : public QWidget QMargins baseMargins; int lineCount; int lineHeight; + QColor background; + QColor foreground; }; const QString InlineAddedIndicator = QStringLiteral("compare_inline_added"); @@ -144,6 +192,11 @@ bool hasLargeAnnotationGap(const QVector &hunks, bool leftSide) } +Overlay::Overlay(AppearanceManager *appearanceManager) : + appearanceManager(appearanceManager) +{ +} + void Overlay::apply(ScintillaNext *newLeftEditor, ScintillaNext *newRightEditor, const QVector &hunks) @@ -275,6 +328,7 @@ Overlay::MarginState Overlay::configureEditor(ScintillaNext *editor, return {}; } + const CompareColors colors = compareColors(appearanceManager); MarginState marginState; marginState.editor = editor; marginState.type = editor->marginTypeN(CompareMargin); @@ -309,7 +363,7 @@ Overlay::MarginState Overlay::configureEditor(ScintillaNext *editor, editor->setMarginTypeN(CompareMargin, SC_MARGIN_SYMBOL); editor->setMarginWidthN(CompareMargin, CompareMarginWidth); editor->setMarginMaskN(CompareMargin, CompareMarkerMask); - editor->setMarginBackN(CompareMargin, CompareMarginBackground); + editor->setMarginBackN(CompareMargin, scintillaColor(colors.marginBackground)); editor->annotationClearAll(); @@ -327,47 +381,47 @@ Overlay::MarginState Overlay::configureEditor(ScintillaNext *editor, } editor->styleSetSizeFractional(annotationStyle, editor->styleSizeFractional(STYLE_DEFAULT)); editor->styleSetWeight(annotationStyle, editor->styleWeight(STYLE_DEFAULT)); - editor->styleSetFore(annotationStyle, GapForeground); - editor->styleSetBack(annotationStyle, GapBackground); + editor->styleSetFore(annotationStyle, scintillaColor(colors.gapForeground)); + editor->styleSetBack(annotationStyle, scintillaColor(colors.gapBackground)); editor->styleSetEOLFilled(annotationStyle, true); editor->annotationSetStyleOffset(annotationStyle); editor->annotationSetVisible(ANNOTATION_STANDARD); - auto configureBackgroundMarker = [editor](int marker, int color) { + auto configureBackgroundMarker = [editor](int marker, const QColor &color) { editor->markerDefine(marker, SC_MARK_BACKGROUND); - editor->markerSetBack(marker, color); + editor->markerSetBack(marker, scintillaColor(color)); editor->markerSetLayer(marker, SC_LAYER_UNDER_TEXT); - editor->markerSetAlpha(marker, 150); + editor->markerSetAlpha(marker, color.alpha()); }; - configureBackgroundMarker(AddedBackgroundMarker, AddedColor); - configureBackgroundMarker(DeletedBackgroundMarker, DeletedColor); + configureBackgroundMarker(AddedBackgroundMarker, colors.addedFill); + configureBackgroundMarker(DeletedBackgroundMarker, colors.deletedFill); editor->markerDefine(AddedMarker, SC_MARK_FULLRECT); - editor->markerSetFore(AddedMarker, AddedColor); - editor->markerSetBack(AddedMarker, AddedColor); + editor->markerSetFore(AddedMarker, scintillaColor(colors.addedMarker)); + editor->markerSetBack(AddedMarker, scintillaColor(colors.addedMarker)); editor->markerDefine(DeletedMarker, SC_MARK_FULLRECT); - editor->markerSetFore(DeletedMarker, DeletedColor); - editor->markerSetBack(DeletedMarker, DeletedColor); + editor->markerSetFore(DeletedMarker, scintillaColor(colors.deletedMarker)); + editor->markerSetBack(DeletedMarker, scintillaColor(colors.deletedMarker)); editor->markerDefine(CurrentMarker, SC_MARK_SHORTARROW); - editor->markerSetFore(CurrentMarker, 0xFFFFFF); - editor->markerSetBack(CurrentMarker, CurrentColor); + editor->markerSetFore(CurrentMarker, scintillaColor(colors.currentForeground)); + editor->markerSetBack(CurrentMarker, scintillaColor(colors.current)); - auto configureInlineIndicator = [editor](const QString &name, int color) { + auto configureInlineIndicator = [editor](const QString &name, const QColor &color) { const int indicator = editor->allocateIndicator(name); editor->indicSetStyle(indicator, INDIC_FULLBOX); - editor->indicSetFore(indicator, color); - editor->indicSetAlpha(indicator, 230); + editor->indicSetFore(indicator, scintillaColor(color)); + editor->indicSetAlpha(indicator, 110); editor->indicSetOutlineAlpha(indicator, 90); editor->indicSetUnder(indicator, true); }; - configureInlineIndicator(InlineAddedIndicator, InlineAddedColor); - configureInlineIndicator(InlineDeletedIndicator, InlineDeletedColor); + configureInlineIndicator(InlineAddedIndicator, colors.addedMarker); + configureInlineIndicator(InlineDeletedIndicator, colors.deletedMarker); const int indicator = editor->allocateIndicator(QStringLiteral("compare_current")); editor->indicSetStyle(indicator, INDIC_STRAIGHTBOX); - editor->indicSetFore(indicator, CurrentColor); + editor->indicSetFore(indicator, scintillaColor(colors.current)); editor->indicSetAlpha(indicator, 0); editor->indicSetOutlineAlpha(indicator, 220); editor->indicSetUnder(indicator, false); @@ -512,8 +566,10 @@ void Overlay::addLeadingGap(ScintillaNext *editor, MarginState &marginState, qsi const int lineHeight = std::max(1, static_cast(editor->textHeight(0))); marginState.leadingGapLines = totalCount; marginState.visibleLeadingGapLines = totalCount; + const CompareColors colors = compareColors(appearanceManager); marginState.topGapWidget = new TopGapWidget( - editor, marginState.viewportMargins, totalCount, lineHeight); + editor, marginState.viewportMargins, totalCount, lineHeight, + colors.gapBackground, colors.gapForeground); } void Overlay::setCurrentMarker(ScintillaNext *editor, qsizetype line) diff --git a/src/compare/CompareOverlay.h b/src/compare/CompareOverlay.h index f808237..51db4b9 100644 --- a/src/compare/CompareOverlay.h +++ b/src/compare/CompareOverlay.h @@ -17,6 +17,7 @@ #include class ScintillaNext; +class AppearanceManager; namespace Compare { @@ -24,6 +25,8 @@ namespace Compare class Overlay { public: + explicit Overlay(AppearanceManager *appearanceManager = nullptr); + void apply(ScintillaNext *leftEditor, ScintillaNext *rightEditor, const QVector &hunks); @@ -60,23 +63,24 @@ class Overlay qsizetype visibleLeadingGapLines = 0; }; - static MarginState configureEditor(ScintillaNext *editor, bool suspendScrollWidthTracking); + MarginState configureEditor(ScintillaNext *editor, bool suspendScrollWidthTracking); static void clearEditor(ScintillaNext *editor, const MarginState &marginState); static void addLines(ScintillaNext *editor, qsizetype start, qsizetype count, int marker); static void fillInlineRanges(ScintillaNext *editor, qsizetype lineStart, const QVector &spans, const QString &indicatorName); - static void addGapLines(ScintillaNext *editor, - MarginState &marginState, - qsizetype anchorLine, - qsizetype count); - static void addLeadingGap(ScintillaNext *editor, MarginState &marginState, qsizetype count); + void addGapLines(ScintillaNext *editor, + MarginState &marginState, + qsizetype anchorLine, + qsizetype count); + void addLeadingGap(ScintillaNext *editor, MarginState &marginState, qsizetype count); static void setCurrentMarker(ScintillaNext *editor, qsizetype line); static void setCurrentRange(ScintillaNext *editor, qsizetype start, qsizetype count); QPointer leftEditor; QPointer rightEditor; + AppearanceManager *appearanceManager; MarginState leftMarginState; MarginState rightMarginState; }; diff --git a/src/compare/CompareSession.cpp b/src/compare/CompareSession.cpp index 574d25e..8c7ed61 100644 --- a/src/compare/CompareSession.cpp +++ b/src/compare/CompareSession.cpp @@ -10,6 +10,7 @@ #include "CompareSession.h" #include "CompareEngine.h" +#include "AppearanceTrace.h" #include "ScintillaNext.h" #include @@ -21,7 +22,13 @@ namespace Compare { Session::Session(QObject *parent) : - QObject(parent) + Session(nullptr, parent) +{ +} + +Session::Session(AppearanceManager *appearanceManager, QObject *parent) : + QObject(parent), + overlay(appearanceManager) { workerPool.setMaxThreadCount(1); workerPool.setExpiryTimeout(-1); @@ -129,6 +136,20 @@ void Session::previousDifference() navigateTo(navigator.previous()); } +void Session::refreshAppearance() +{ + if (currentState != State::Ready || !hasPair()) { + return; + } + + AppearanceTrace::Scope trace(QStringLiteral("compare-overlay"), + QStringLiteral("hunks=%1").arg(currentResult.hunks.size())); + applyingOverlay = true; + overlay.apply(leftEditor, rightEditor, currentResult.hunks); + overlay.setCurrent(navigator.current()); + applyingOverlay = false; +} + void Session::beginComputation() { stopWorker(); diff --git a/src/compare/CompareSession.h b/src/compare/CompareSession.h index 97fae8a..717e65b 100644 --- a/src/compare/CompareSession.h +++ b/src/compare/CompareSession.h @@ -18,6 +18,7 @@ #include class ScintillaNext; +class AppearanceManager; namespace Scintilla { @@ -44,6 +45,7 @@ class Session : public QObject Q_ENUM(State) explicit Session(QObject *parent = nullptr); + Session(AppearanceManager *appearanceManager, QObject *parent); ~Session() override; void start(ScintillaNext *leftEditor, ScintillaNext *rightEditor); @@ -52,6 +54,7 @@ class Session : public QObject void clear(); void nextDifference(); void previousDifference(); + void refreshAppearance(); State state() const { return currentState; } bool hasPair() const { return !leftEditor.isNull() && !rightEditor.isNull(); } diff --git a/src/compare/CompareToolBar.cpp b/src/compare/CompareToolBar.cpp index d9b406a..49f777c 100644 --- a/src/compare/CompareToolBar.cpp +++ b/src/compare/CompareToolBar.cpp @@ -9,11 +9,15 @@ #include "CompareToolBar.h" +#include "AppearanceManager.h" +#include "AppearanceTrace.h" #include "CompareSession.h" #include +#include #include #include +#include #include #include @@ -21,7 +25,49 @@ namespace Compare { +namespace +{ + +QString cssColor(const QColor &color) +{ + return QStringLiteral("rgba(%1, %2, %3, %4)") + .arg(color.red()) + .arg(color.green()) + .arg(color.blue()) + .arg(color.alpha()); +} + +QIcon monochromeIcon(const QString &resource, const QColor &color) +{ + const QIcon source(resource); + QIcon result; + for (const int size : {16, 20, 24, 32}) { + QPixmap pixmap = source.pixmap(size, size); + QPainter painter(&pixmap); + painter.setCompositionMode(QPainter::CompositionMode_SourceIn); + painter.fillRect(pixmap.rect(), color); + painter.end(); + result.addPixmap(pixmap); + } + return result; +} + +} + +CompareToolBar::CompareToolBar(Session *session, + QAction *previousAction, + QAction *nextAction, + QAction *refreshAction, + QAction *cancelAction, + QAction *clearAction, + QWidget *parent) : + CompareToolBar(session, nullptr, previousAction, nextAction, refreshAction, + cancelAction, clearAction, parent) +{ +} + CompareToolBar::CompareToolBar(Session *session, + AppearanceManager *appearanceManager, QAction *previousAction, QAction *nextAction, QAction *refreshAction, @@ -30,6 +76,7 @@ CompareToolBar::CompareToolBar(Session *session, QWidget *parent) : QToolBar(tr("Compare"), parent), session(session), + appearanceManager(appearanceManager), previousAction(previousAction), nextAction(nextAction), refreshAction(refreshAction), @@ -38,33 +85,35 @@ CompareToolBar::CompareToolBar(Session *session, pairLabel(new QLabel(this)), statusLabel(new QLabel(this)) { + if (!appearanceManager) { + const QPalette palette = QApplication::palette(); + fallbackTokens.surfaceShell = palette.color(QPalette::Window); + fallbackTokens.surfaceRaised = palette.color(QPalette::Button); + fallbackTokens.surfaceHover = palette.color(QPalette::AlternateBase); + fallbackTokens.borderDefault = palette.color(QPalette::Mid); + fallbackTokens.textPrimary = palette.color(QPalette::WindowText); + fallbackTokens.textSecondary = palette.color(QPalette::PlaceholderText); + fallbackTokens.stateError = QColor(QStringLiteral("#A1260D")); + fallbackTokens.stateWarning = QColor(QStringLiteral("#8A6D00")); + fallbackTokens.stateSuccess = QColor(QStringLiteral("#107C10")); + fallbackTokens.stateInformation = palette.color(QPalette::Link); + fallbackTokens.diffAddedMarker = QColor(QStringLiteral("#2F9E44")); + fallbackTokens.diffModifiedMarker = QColor(QStringLiteral("#0078D4")); + fallbackTokens.diffDeletedMarker = QColor(QStringLiteral("#E03131")); + } + setObjectName(QStringLiteral("compareToolBar")); setMovable(false); setFloatable(false); setIconSize(QSize(16, 16)); setToolButtonStyle(Qt::ToolButtonIconOnly); - setStyleSheet(QStringLiteral( - "QToolBar#compareToolBar { background: #f8fafc; border-top: 1px solid #e2e8f0; " - "border-bottom: 1px solid #cbd5e1; padding: 5px 8px; spacing: 3px; }" - "QToolBar#compareToolBar QToolButton { border: 1px solid transparent; border-radius: 5px; " - "min-width: 26px; min-height: 26px; padding: 1px; }" - "QToolBar#compareToolBar QToolButton:hover { background: #e8edf2; border-color: #d8e0e7; }" - "QToolBar#compareToolBar QToolButton:pressed { background: #dce4ea; }" - "QToolBar#compareToolBar QToolButton:disabled { opacity: 0.38; }" - "QToolBar#compareToolBar::separator { background: #d9e1e7; width: 1px; margin: 5px 6px; }" - "QLabel#comparePairLabel { color: #1f2937; font-weight: 600; padding: 0 10px 0 2px; }")); - - previousAction->setIcon(QIcon(QStringLiteral(":/icons/chevron-up.svg"))); + previousAction->setShortcut(QKeySequence(QStringLiteral("Shift+F7"))); previousAction->setToolTip(tr("Previous Difference (Shift+F7)")); - nextAction->setIcon(QIcon(QStringLiteral(":/icons/chevron-down.svg"))); nextAction->setShortcut(QKeySequence(QStringLiteral("F7"))); nextAction->setToolTip(tr("Next Difference (F7)")); - refreshAction->setIcon(QIcon(QStringLiteral(":/icons/rotate-cw.svg"))); refreshAction->setToolTip(tr("Refresh Compare")); - cancelAction->setIcon(QIcon(QStringLiteral(":/icons/cross.svg"))); cancelAction->setToolTip(tr("Cancel Compare")); - clearAction->setIcon(QIcon(QStringLiteral(":/icons/trash-2.svg"))); clearAction->setToolTip(tr("Clear Compare")); pairLabel->setObjectName(QStringLiteral("comparePairLabel")); @@ -76,7 +125,7 @@ CompareToolBar::CompareToolBar(Session *session, statusLabel->setAlignment(Qt::AlignCenter); statusLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); - auto createMetric = [this](const QString &name, const QString &color, QLabel **textLabel) { + auto createMetric = [this](const QString &name, const QString &dotName, QLabel **textLabel) { QWidget *metric = new QWidget(this); metric->setObjectName(name + QStringLiteral("Metric")); metric->setFixedHeight(24); @@ -87,12 +136,11 @@ CompareToolBar::CompareToolBar(Session *session, layout->setSpacing(5); QLabel *dot = new QLabel(metric); + dot->setObjectName(dotName); dot->setFixedSize(7, 7); - dot->setStyleSheet(QStringLiteral("background: %1; border-radius: 3px;").arg(color)); QLabel *label = new QLabel(metric); label->setObjectName(name); - label->setStyleSheet(QStringLiteral("color: #475569; font-weight: 500;")); label->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); layout->addWidget(dot); @@ -101,11 +149,11 @@ CompareToolBar::CompareToolBar(Session *session, return metric; }; addedMetric = createMetric( - QStringLiteral("compareAddedCountLabel"), QStringLiteral("#2f9e44"), &addedCountLabel); + QStringLiteral("compareAddedCountLabel"), QStringLiteral("compareAddedDot"), &addedCountLabel); deletedMetric = createMetric( - QStringLiteral("compareDeletedCountLabel"), QStringLiteral("#e03131"), &deletedCountLabel); + QStringLiteral("compareDeletedCountLabel"), QStringLiteral("compareDeletedDot"), &deletedCountLabel); modifiedMetric = createMetric( - QStringLiteral("compareModifiedCountLabel"), QStringLiteral("#d97706"), &modifiedCountLabel); + QStringLiteral("compareModifiedCountLabel"), QStringLiteral("compareModifiedDot"), &modifiedCountLabel); QWidget *spacer = new QWidget(this); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); @@ -129,9 +177,46 @@ CompareToolBar::CompareToolBar(Session *session, connect(session, &Session::stateChanged, this, [this]() { updateState(); }); connect(session, &Session::navigationChanged, this, [this]() { updateState(); }); + applyAppearance(); +} + +void CompareToolBar::applyAppearance() +{ + AppearanceTrace::Scope trace(QStringLiteral("compare-toolbar")); + const AppearanceTokens &tokens = appearanceTokens(); + setStyleSheet(QStringLiteral( + "QToolBar#compareToolBar { background: %1; border-top: 1px solid %2; " + "border-bottom: 1px solid %2; padding: 5px 8px; spacing: 3px; }" + "QToolBar#compareToolBar QToolButton { color: %3; border: 1px solid transparent; " + "border-radius: 5px; min-width: 26px; min-height: 26px; padding: 1px; }" + "QToolBar#compareToolBar QToolButton:hover { background: %4; border-color: %2; }" + "QToolBar#compareToolBar QToolButton:pressed { background: %5; }" + "QToolBar#compareToolBar::separator { background: %2; width: 1px; margin: 5px 6px; }" + "QLabel#comparePairLabel { color: %3; font-weight: 600; padding: 0 10px 0 2px; }" + "QLabel#compareAddedCountLabel, QLabel#compareDeletedCountLabel, " + "QLabel#compareModifiedCountLabel { color: %6; font-weight: 500; }" + "QLabel#compareAddedDot { background: %7; border-radius: 3px; }" + "QLabel#compareDeletedDot { background: %8; border-radius: 3px; }" + "QLabel#compareModifiedDot { background: %9; border-radius: 3px; }") + .arg(cssColor(tokens.surfaceShell), cssColor(tokens.borderDefault), + cssColor(tokens.textPrimary), cssColor(tokens.surfaceHover), + cssColor(tokens.surfaceRaised), cssColor(tokens.textSecondary), + cssColor(tokens.diffAddedMarker), cssColor(tokens.diffDeletedMarker), + cssColor(tokens.diffModifiedMarker))); + + previousAction->setIcon(monochromeIcon(QStringLiteral(":/icons/chevron-up.svg"), tokens.textPrimary)); + nextAction->setIcon(monochromeIcon(QStringLiteral(":/icons/chevron-down.svg"), tokens.textPrimary)); + refreshAction->setIcon(monochromeIcon(QStringLiteral(":/icons/rotate-cw.svg"), tokens.textPrimary)); + cancelAction->setIcon(monochromeIcon(QStringLiteral(":/icons/cross.svg"), tokens.textPrimary)); + clearAction->setIcon(monochromeIcon(QStringLiteral(":/icons/trash-2.svg"), tokens.textPrimary)); updateState(); } +const AppearanceTokens &CompareToolBar::appearanceTokens() const +{ + return appearanceManager ? appearanceManager->tokens() : fallbackTokens; +} + void CompareToolBar::updateState() { const bool hasPair = session->hasPair(); @@ -158,22 +243,26 @@ void CompareToolBar::updateState() modifiedCountLabel->setText(tr("%1 changed").arg(session->modifiedCount())); QString statusText; - QString statusBackground = QStringLiteral("#eef2f6"); - QString statusBorder = QStringLiteral("#d8e0e7"); - QString statusForeground = QStringLiteral("#475569"); + const AppearanceTokens &tokens = appearanceTokens(); + QColor statusBackground = tokens.surfaceRaised; + QColor statusBorder = tokens.borderDefault; + QColor statusForeground = tokens.textSecondary; + auto useStateColor = [&](const QColor &color) { + statusForeground = color; + statusBackground = color; + statusBackground.setAlpha(36); + statusBorder = color; + statusBorder.setAlpha(120); + }; switch (session->state()) { case Session::State::Running: statusText = tr("Comparing"); - statusBackground = QStringLiteral("#e8f1ff"); - statusBorder = QStringLiteral("#bfd5f5"); - statusForeground = QStringLiteral("#1d4ed8"); + useStateColor(tokens.stateInformation); break; case Session::State::Ready: if (session->differenceCount() == 0) { statusText = tr("No changes"); - statusBackground = QStringLiteral("#eaf7ee"); - statusBorder = QStringLiteral("#b9dec5"); - statusForeground = QStringLiteral("#287a3e"); + useStateColor(tokens.stateSuccess); } else if (session->currentDifferenceNumber() > 0) { statusText = tr("%1 / %2") @@ -187,19 +276,14 @@ void CompareToolBar::updateState() case Session::State::Stale: statusText = tr("Out of date"); statusLabel->setToolTip(tr("Refresh Compare to update the result.")); - statusBackground = QStringLiteral("#fff6e5"); - statusBorder = QStringLiteral("#efd39a"); - statusForeground = QStringLiteral("#8a5a00"); + useStateColor(tokens.stateWarning); break; case Session::State::Cancelled: statusText = tr("Cancelled"); - statusBackground = QStringLiteral("#eef2f6"); break; case Session::State::Failed: statusText = tr("Compare failed"); - statusBackground = QStringLiteral("#fdecec"); - statusBorder = QStringLiteral("#efb8b8"); - statusForeground = QStringLiteral("#a22626"); + useStateColor(tokens.stateError); break; case Session::State::None: break; @@ -211,7 +295,7 @@ void CompareToolBar::updateState() statusLabel->setStyleSheet(QStringLiteral( "color: %1; background: %2; border: 1px solid %3; border-radius: 10px; " "font-weight: 500; padding: 3px 10px;") - .arg(statusForeground, statusBackground, statusBorder)); + .arg(cssColor(statusForeground), cssColor(statusBackground), cssColor(statusBorder))); statusLabel->setMinimumWidth(std::max(74, statusLabel->sizeHint().width())); } diff --git a/src/compare/CompareToolBar.h b/src/compare/CompareToolBar.h index 22d38d9..b69947b 100644 --- a/src/compare/CompareToolBar.h +++ b/src/compare/CompareToolBar.h @@ -9,6 +9,8 @@ #pragma once +#include "AppearanceManager.h" + #include class QAction; @@ -30,11 +32,24 @@ class CompareToolBar : public QToolBar QAction *cancelAction, QAction *clearAction, QWidget *parent = nullptr); + CompareToolBar(Session *session, + AppearanceManager *appearanceManager, + QAction *previousAction, + QAction *nextAction, + QAction *refreshAction, + QAction *cancelAction, + QAction *clearAction, + QWidget *parent = nullptr); + + void applyAppearance(); private: + const AppearanceTokens &appearanceTokens() const; void updateState(); Session *session; + AppearanceManager *appearanceManager; + AppearanceTokens fallbackTokens; QAction *previousAction; QAction *nextAction; QAction *refreshAction; diff --git a/src/decorators/BookMarkDecorator.cpp b/src/decorators/BookMarkDecorator.cpp index 65dfc03..c1c6b6e 100644 --- a/src/decorators/BookMarkDecorator.cpp +++ b/src/decorators/BookMarkDecorator.cpp @@ -28,8 +28,6 @@ BookMarkDecorator::BookMarkDecorator(ScintillaNext *editor) : { editor->markerSetAlpha(MARK_BOOKMARK, 70); editor->markerDefine(MARK_BOOKMARK, SC_MARK_BOOKMARK); - editor->markerSetFore(MARK_BOOKMARK, 0xFF2020); - editor->markerSetBack(MARK_BOOKMARK, 0xFF2020); const int mask = editor->marginMaskN(MARGIN); editor->setMarginMaskN(MARGIN, (1 << MARK_BOOKMARK) | mask); diff --git a/src/decorators/BraceMatch.cpp b/src/decorators/BraceMatch.cpp index 981d342..1f41b39 100644 --- a/src/decorators/BraceMatch.cpp +++ b/src/decorators/BraceMatch.cpp @@ -33,14 +33,12 @@ BraceMatch::BraceMatch(ScintillaNext *editor) : const int braceBadlight = editor->allocateIndicator("brace_badlight"); editor->indicSetStyle(braceHighlight, INDIC_FULLBOX); - editor->indicSetFore(braceHighlight, 0x999999); editor->indicSetAlpha(braceHighlight, 100); editor->indicSetOutlineAlpha(braceHighlight, 200); editor->indicSetUnder(braceHighlight, true); editor->braceHighlightIndicator(true, braceHighlight); editor->indicSetStyle(braceBadlight, INDIC_FULLBOX); - editor->indicSetFore(braceBadlight, 0x0000FF); editor->indicSetAlpha(braceBadlight, 100); editor->indicSetOutlineAlpha(braceBadlight, 200); editor->indicSetUnder(braceBadlight, true); diff --git a/src/decorators/HighlightedScrollBar.cpp b/src/decorators/HighlightedScrollBar.cpp index e268f42..abcc50d 100644 --- a/src/decorators/HighlightedScrollBar.cpp +++ b/src/decorators/HighlightedScrollBar.cpp @@ -19,6 +19,7 @@ #include +#include "AppearanceManager.h" #include "HighlightedScrollBar.h" @@ -26,14 +27,10 @@ using namespace Scintilla; const int DEFAULT_TICK_HEIGHT = 3; const int DEFAULT_TICK_PADDING = 3; -const QColor CURSOR_SELECTION_COLOR = QColor(0, 0, 0, 25); -const QColor CURSOR_CARET_COLOR = QColor(0, 0, 0, 100); -const QColor COMPARE_ADDED_COLOR = QColor(49, 153, 78); -const QColor COMPARE_DELETED_COLOR = QColor(211, 63, 63); -const QColor COMPARE_CURRENT_COLOR = QColor(46, 125, 233); - -HighlightedScrollBarDecorator::HighlightedScrollBarDecorator(ScintillaNext *editor) - : EditorDecorator(editor), scrollBar(new HighlightedScrollBar(editor, Qt::Vertical, editor)) +HighlightedScrollBarDecorator::HighlightedScrollBarDecorator( + ScintillaNext *editor, AppearanceManager *appearanceManager) + : EditorDecorator(editor), + scrollBar(new HighlightedScrollBar(editor, appearanceManager, Qt::Vertical, editor)) { connect(scrollBar, &QScrollBar::valueChanged, editor, &ScintillaEditBase::scrollVertical); @@ -57,8 +54,13 @@ void HighlightedScrollBarDecorator::notify(const NotificationData *pscn) -HighlightedScrollBar::HighlightedScrollBar(ScintillaNext *editor, Qt::Orientation orientation, QWidget *parent) - : QScrollBar(orientation, parent), editor(editor) +HighlightedScrollBar::HighlightedScrollBar(ScintillaNext *editor, + AppearanceManager *appearanceManager, + Qt::Orientation orientation, + QWidget *parent) + : QScrollBar(orientation, parent), + editor(editor), + appearanceManager(appearanceManager) { smartHighlighterIndicator = editor->allocateIndicator("smart_highlighter"); } @@ -68,11 +70,12 @@ void HighlightedScrollBar::paintEvent(QPaintEvent *event) // Paint the default scrollbar first QScrollBar::paintEvent(event); QPainter p(this); + const AppearanceTokens &tokens = appearanceManager->tokens(); - drawMarker(p, 24, QColor(100, 100, 255)); - drawMarker(p, 16, COMPARE_ADDED_COLOR, 5); - drawMarker(p, 17, COMPARE_DELETED_COLOR, 5); - drawMarker(p, 19, COMPARE_CURRENT_COLOR, 7); + drawMarker(p, 24, tokens.diffModifiedMarker); + drawMarker(p, 16, tokens.diffAddedMarker, 5); + drawMarker(p, 17, tokens.diffDeletedMarker, 5); + drawMarker(p, 19, tokens.accentPrimary, 7); drawIndicator(p, smartHighlighterIndicator); drawCursors(p); } @@ -104,15 +107,18 @@ void HighlightedScrollBar::drawIndicator(QPainter &p, int indicator) void HighlightedScrollBar::drawCursors(QPainter &p) { + QColor selectionColor = appearanceManager->tokens().selectionInactive; + QColor caretColor = appearanceManager->tokens().editorCaret; + caretColor.setAlpha(130); for (int i = 0; i < editor->selections() ; i++) { int startCaretY = posToScrollBarY(editor->selectionNCaret(i)); int startAnchorY = posToScrollBarY(editor->selectionNAnchor(i)); if (startCaretY != startAnchorY) { - drawTickMark(p, startAnchorY, startCaretY - startAnchorY, CURSOR_SELECTION_COLOR); + drawTickMark(p, startAnchorY, startCaretY - startAnchorY, selectionColor); } - drawTickMark(p, startCaretY, DEFAULT_TICK_HEIGHT, CURSOR_CARET_COLOR); + drawTickMark(p, startCaretY, DEFAULT_TICK_HEIGHT, caretColor); } } diff --git a/src/decorators/HighlightedScrollBar.h b/src/decorators/HighlightedScrollBar.h index 1815801..22eec84 100644 --- a/src/decorators/HighlightedScrollBar.h +++ b/src/decorators/HighlightedScrollBar.h @@ -27,13 +27,15 @@ class HighlightedScrollBar; +class AppearanceManager; class HighlightedScrollBarDecorator : public EditorDecorator { Q_OBJECT public: - explicit HighlightedScrollBarDecorator(ScintillaNext *editor); + explicit HighlightedScrollBarDecorator(ScintillaNext *editor, + AppearanceManager *appearanceManager); ~HighlightedScrollBarDecorator() override; public slots: @@ -49,7 +51,10 @@ class HighlightedScrollBar : public QScrollBar Q_OBJECT public: - explicit HighlightedScrollBar(ScintillaNext *editor, Qt::Orientation orientation, QWidget *parent = nullptr); + explicit HighlightedScrollBar(ScintillaNext *editor, + AppearanceManager *appearanceManager, + Qt::Orientation orientation, + QWidget *parent = nullptr); protected: void paintEvent(QPaintEvent *event) override; @@ -66,6 +71,7 @@ class HighlightedScrollBar : public QScrollBar int scrollbarArrowHeight() const; ScintillaNext *editor; + AppearanceManager *appearanceManager; int smartHighlighterIndicator; }; diff --git a/src/decorators/MarkerAppDecorator.cpp b/src/decorators/MarkerAppDecorator.cpp index 7df9cc0..2921390 100644 --- a/src/decorators/MarkerAppDecorator.cpp +++ b/src/decorators/MarkerAppDecorator.cpp @@ -17,40 +17,39 @@ */ #include "MarkerAppDecorator.h" +#include "AppearanceManager.h" #include "EditorManager.h" #include "ScintillaNext.h" +MarkerAppDecorator::MarkerAppDecorator(NotepadSharpApplication *app) + : ApplicationDecorator(app) +{ + connect(app->getEditorManager(), &EditorManager::editorCreated, + this, [this](ScintillaNext *editor) { applyAppearance(editor); }); +} -static QList marker_colors = { - QColor(0x00, 0xFF, 0xFF), - QColor(0xFF, 0x80, 0x00), - QColor(0xFF, 0xFF, 0x00) -}; - -static int QColorToScintillaColour(QColor c) +QColor MarkerAppDecorator::markerColor(int i) const { - return c.red() | (c.green() << 8) | (c.blue() << 16); + return markerColors().value(i); } -MarkerAppDecorator::MarkerAppDecorator(NotepadSharpApplication *app) - : ApplicationDecorator(app) +QList MarkerAppDecorator::markerColors() const { - // Any time an editor is created go ahead and allocate/set the required indicators - connect(app->getEditorManager(), &EditorManager::editorCreated, this, [](ScintillaNext *editor) { - for (int i = 0; i < marker_colors.size(); i++) { - int indicator = editor->allocateIndicator(QString("marker_%1").arg(i)); - editor->indicSetFore(indicator, QColorToScintillaColour(marker_colors[i])); - editor->indicSetStyle(indicator, INDIC_ROUNDBOX); - editor->indicSetOutlineAlpha(indicator, 150); - editor->indicSetAlpha(indicator, 100); - editor->indicSetUnder(indicator, true); - } - }); + const AppearanceTokens &tokens = app->getAppearanceManager()->tokens(); + return {tokens.syntaxString, tokens.syntaxVariable, tokens.syntaxTag}; } -QColor MarkerAppDecorator::markerColor(int i) const +void MarkerAppDecorator::applyAppearance(ScintillaNext *editor) const { - return marker_colors[i]; + const QList colors = markerColors(); + for (int index = 0; index < colors.size(); ++index) { + const int indicator = editor->allocateIndicator(QStringLiteral("marker_%1").arg(index)); + editor->indicSetFore(indicator, AppearanceManager::scintillaColor(colors[index])); + editor->indicSetStyle(indicator, INDIC_ROUNDBOX); + editor->indicSetOutlineAlpha(indicator, 150); + editor->indicSetAlpha(indicator, 100); + editor->indicSetUnder(indicator, true); + } } void MarkerAppDecorator::mark(ScintillaNext *editor, int i) @@ -99,7 +98,7 @@ void MarkerAppDecorator::clear(ScintillaNext *editor, int i) void MarkerAppDecorator::clearAll(ScintillaNext *editor) { - for (int i = 0; i < marker_colors.size(); i++) { + for (int i = 0; i < markerColors().size(); i++) { clear(editor, i); } } \ No newline at end of file diff --git a/src/decorators/MarkerAppDecorator.h b/src/decorators/MarkerAppDecorator.h index d60d48d..7edfdd2 100644 --- a/src/decorators/MarkerAppDecorator.h +++ b/src/decorators/MarkerAppDecorator.h @@ -32,4 +32,8 @@ class MarkerAppDecorator : public ApplicationDecorator void mark(ScintillaNext *editor, int i); void clear(ScintillaNext *editor, int i); void clearAll(ScintillaNext *editor); + +private: + QList markerColors() const; + void applyAppearance(ScintillaNext *editor) const; }; diff --git a/src/decorators/SmartHighlighter.cpp b/src/decorators/SmartHighlighter.cpp index 9924e63..592d627 100644 --- a/src/decorators/SmartHighlighter.cpp +++ b/src/decorators/SmartHighlighter.cpp @@ -29,7 +29,6 @@ SmartHighlighter::SmartHighlighter(ScintillaNext *editor) : indicator = editor->allocateIndicator("smart_highlighter"); - editor->indicSetFore(indicator, 0x00FF00); editor->indicSetStyle(indicator, INDIC_ROUNDBOX); editor->indicSetOutlineAlpha(indicator, 150); editor->indicSetAlpha(indicator, 100); diff --git a/src/decorators/URLFinder.cpp b/src/decorators/URLFinder.cpp index f0b2242..23fc899 100644 --- a/src/decorators/URLFinder.cpp +++ b/src/decorators/URLFinder.cpp @@ -31,10 +31,8 @@ URLFinder::URLFinder(ScintillaNext *editor) : indicator = editor->allocateIndicator("url_finder"); editor->indicSetStyle(indicator, INDIC_PLAIN); - editor->indicSetFore(indicator, 0xFF0000); editor->indicSetHoverStyle(indicator, INDIC_DOTS); - editor->indicSetHoverFore(indicator, 0xFF0000); // Resizing the window could reveal more text connect(editor, &ScintillaNext::resized, timer, qOverload<>(&QTimer::start)); diff --git a/src/dialogs/FindReplaceDialog.cpp b/src/dialogs/FindReplaceDialog.cpp index b96f415..137cf19 100644 --- a/src/dialogs/FindReplaceDialog.cpp +++ b/src/dialogs/FindReplaceDialog.cpp @@ -18,6 +18,8 @@ #include "FindReplaceDialog.h" +#include "AppearanceManager.h" +#include "AppearanceTrace.h" #include "ApplicationSettings.h" #include "ui_FindReplaceDialog.h" @@ -43,9 +45,12 @@ static void convertToExtended(QString &str) // TODO: more } -FindReplaceDialog::FindReplaceDialog(ISearchResultsHandler *searchResults, MainWindow *window) : +FindReplaceDialog::FindReplaceDialog(ISearchResultsHandler *searchResults, + AppearanceManager *appearanceManager, + MainWindow *window) : QDialog(window, Qt::Dialog), ui(new Ui::FindReplaceDialog), + appearanceManager(appearanceManager), searchResultsHandler(searchResults), finder(new Finder(window->currentEditor())) { @@ -70,6 +75,8 @@ FindReplaceDialog::FindReplaceDialog(ISearchResultsHandler *searchResults, MainW statusBar = new QStatusBar(); statusBar->setSizeGripEnabled(false); // the dialog has one already qobject_cast(layout())->insertWidget(-1, statusBar); + connect(appearanceManager, &AppearanceManager::effectiveAppearanceChanged, + this, &FindReplaceDialog::applyAppearance); // Disable auto completion ui->comboFind->setCompleter(nullptr); @@ -706,7 +713,8 @@ int FindReplaceDialog::computeSearchFlags() int FindReplaceDialog::ensureMarkIndicator() { int markIndicator = editor->allocateIndicator(QStringLiteral("find_mark_highlight")); - editor->indicSetFore(markIndicator, 0xFFCC00); + editor->indicSetFore( + markIndicator, AppearanceManager::scintillaColor(appearanceManager->tokens().accentHover)); editor->indicSetStyle(markIndicator, INDIC_FULLBOX); editor->indicSetOutlineAlpha(markIndicator, 200); editor->indicSetAlpha(markIndicator, 100); @@ -812,6 +820,23 @@ void FindReplaceDialog::copyMarkedText() void FindReplaceDialog::showMessage(const QString &message, const QString &color) { - statusBar->setStyleSheet(QStringLiteral("color: %1").arg(color)); + statusColorRole = color; + applyAppearance(); statusBar->showMessage(message); } + +void FindReplaceDialog::applyAppearance() +{ + AppearanceTrace::Scope trace(QStringLiteral("find-replace")); + const AppearanceTokens &tokens = appearanceManager->tokens(); + QColor statusColor = tokens.stateInformation; + if (statusColorRole == QStringLiteral("green")) + statusColor = tokens.stateSuccess; + else if (statusColorRole == QStringLiteral("red")) + statusColor = tokens.stateError; + + statusBar->setStyleSheet(QStringLiteral("color: %1") + .arg(statusColor.name(QColor::HexRgb))); + if (editor) + ensureMarkIndicator(); +} diff --git a/src/dialogs/FindReplaceDialog.h b/src/dialogs/FindReplaceDialog.h index 85f1e6e..f8b4872 100644 --- a/src/dialogs/FindReplaceDialog.h +++ b/src/dialogs/FindReplaceDialog.h @@ -32,6 +32,7 @@ class ScintillaNext; class MainWindow; class BookMarkDecorator; +class AppearanceManager; namespace Ui { class FindReplaceDialog; @@ -54,7 +55,9 @@ class FindReplaceDialog : public QDialog Backwards }; - explicit FindReplaceDialog(ISearchResultsHandler *searchResults, MainWindow *window = nullptr); + explicit FindReplaceDialog(ISearchResultsHandler *searchResults, + AppearanceManager *appearanceManager, + MainWindow *window = nullptr); ~FindReplaceDialog() override; void setFindString(const QString &string); @@ -109,6 +112,7 @@ private slots: int computeSearchFlags(); void showMessage(const QString &message, const QString &color); + void applyAppearance(); int ensureMarkIndicator(); BookMarkDecorator *bookMarkDecorator() const; void clearAllBookmarks(); @@ -122,6 +126,8 @@ private slots: ScintillaNext *editor; QStatusBar *statusBar; + AppearanceManager *appearanceManager; + QString statusColorRole; QTabBar *tabBar; ISearchResultsHandler *searchResultsHandler; Finder *finder; diff --git a/src/dialogs/MacroEditorDialog.cpp b/src/dialogs/MacroEditorDialog.cpp index 04d2452..95afc11 100644 --- a/src/dialogs/MacroEditorDialog.cpp +++ b/src/dialogs/MacroEditorDialog.cpp @@ -102,11 +102,15 @@ void MacroEditorDialog::macroNameChanged(const QString &text) { // Ensure there is a non-blank name if (text.trimmed().isEmpty()) { - ui->editMacroName->setStyleSheet("QLineEdit{border: 2px solid red}"); + ui->editMacroName->setProperty("validationError", true); + ui->editMacroName->style()->unpolish(ui->editMacroName); + ui->editMacroName->style()->polish(ui->editMacroName); return; } else { - ui->editMacroName->setStyleSheet(""); + ui->editMacroName->setProperty("validationError", false); + ui->editMacroName->style()->unpolish(ui->editMacroName); + ui->editMacroName->style()->polish(ui->editMacroName); } QModelIndex currentIndex = ui->listMacros->selectionModel()->currentIndex(); diff --git a/src/dialogs/MainWindow.cpp b/src/dialogs/MainWindow.cpp index e2d8d57..87b2063 100644 --- a/src/dialogs/MainWindow.cpp +++ b/src/dialogs/MainWindow.cpp @@ -53,6 +53,8 @@ #include "DockAreaWidget.h" #include "NotepadSharpApplication.h" +#include "AppearanceManager.h" +#include "AppearanceTrace.h" #include "ApplicationSettings.h" #include "ScintillaNext.h" @@ -124,7 +126,7 @@ MainWindow::MainWindow(NotepadSharpApplication *app) : connect(dockedEditor, &DockedEditor::contextMenuRequestedForEditor, this, &MainWindow::tabBarRightClicked); connect(dockedEditor, &DockedEditor::titleBarDoubleClicked, this, &MainWindow::newFile); - compareSession = new Compare::Session(this); + compareSession = new Compare::Session(app->getAppearanceManager(), this); QMenu *compareMenu = new QMenu(tr("&Compare"), this); ui->menuBar->insertMenu(ui->menuSettings->menuAction(), compareMenu); @@ -146,12 +148,15 @@ MainWindow::MainWindow(NotepadSharpApplication *app) : Compare::CompareToolBar *compareToolBar = new Compare::CompareToolBar( compareSession, + app->getAppearanceManager(), previousDifferenceAction, nextDifferenceAction, refreshCompareAction, cancelCompareAction, clearCompareAction, this); + connect(app->getAppearanceManager(), &AppearanceManager::effectiveAppearanceChanged, + compareToolBar, &Compare::CompareToolBar::applyAppearance); addToolBarBreak(Qt::TopToolBarArea); addToolBar(Qt::TopToolBarArea, compareToolBar); @@ -171,6 +176,8 @@ MainWindow::MainWindow(NotepadSharpApplication *app) : connect(compareSession, &Compare::Session::message, this, [this](const QString &text) { ui->statusBar->showMessage(text, 5000); }); + connect(app->getAppearanceManager(), &AppearanceManager::effectiveAppearanceChanged, + compareSession, &Compare::Session::refreshAppearance); auto updateComparePreviousAction = [this]() { comparePreviousAction->setEnabled( @@ -446,7 +453,7 @@ MainWindow::MainWindow(NotepadSharpApplication *app) : ui->statusBar->refresh(currentEditor()); }); - SearchResultsDock *srDock = new SearchResultsDock(this); + SearchResultsDock *srDock = new SearchResultsDock(app->getAppearanceManager(), this); addDockWidget(Qt::BottomDockWidgetArea, srDock); srDock->toggleViewAction()->setShortcut(Qt::Key_F7); ui->menuView->addAction(srDock->toggleViewAction()); @@ -485,7 +492,7 @@ MainWindow::MainWindow(NotepadSharpApplication *app) : QuickFindWidget *quickFind = findChild(QString(), Qt::FindDirectChildrenOnly); if (quickFind == Q_NULLPTR) { - quickFind = new QuickFindWidget(this); + quickFind = new QuickFindWidget(this->app->getAppearanceManager(), this); } quickFind->setEditor(currentEditor()); @@ -1006,7 +1013,8 @@ MainWindow::MainWindow(NotepadSharpApplication *app) : languageInspectorDock->hide(); addDockWidget(Qt::RightDockWidgetArea, languageInspectorDock); - LuaConsoleDock *luaConsoleDock = new LuaConsoleDock(app->getLuaState(), this); + LuaConsoleDock *luaConsoleDock = new LuaConsoleDock( + app->getLuaState(), app->getAppearanceManager(), this); luaConsoleDock->hide(); addDockWidget(Qt::BottomDockWidgetArea, luaConsoleDock); @@ -1038,6 +1046,8 @@ MainWindow::MainWindow(NotepadSharpApplication *app) : }); connect(app->getSettings(), &ApplicationSettings::showToolBarChanged, ui->mainToolBar, &QToolBar::setVisible); connect(app->getSettings(), &ApplicationSettings::showStatusBarChanged, ui->statusBar, &QStatusBar::setVisible); + connect(app->getAppearanceManager(), &AppearanceManager::effectiveAppearanceChanged, + this, &MainWindow::applyStyleSheet); connect(ui->statusBar, &EditorInfoStatusBar::customContextMenuRequestedForEOLLabel, this, [this](const QPoint &pos){ ui->menuEOLConversion->popup(pos); }); @@ -1763,7 +1773,8 @@ void MainWindow::showFindReplaceDialog(int index) FindReplaceDialog *frd = findChild(QString(), Qt::FindDirectChildrenOnly); if (frd == Q_NULLPTR) { - frd = new FindReplaceDialog(determineSearchResultsHandler(), this); + frd = new FindReplaceDialog( + determineSearchResultsHandler(), app->getAppearanceManager(), this); } else { frd->setSearchResultsHandler(determineSearchResultsHandler()); @@ -1990,27 +2001,41 @@ void MainWindow::activateEditor(ScintillaNext *editor) void MainWindow::applyStyleSheet() { qInfo(Q_FUNC_INFO); + AppearanceTrace::Scope totalTrace(QStringLiteral("stylesheet-total")); QString sheet; - QFile f(":/stylesheets/npp.css"); - qInfo() << "Loading stylesheet:" << f.fileName(); - - f.open(QFile::ReadOnly); - sheet = f.readAll(); - f.close(); - - // If there is a "custom.css" file where the ini is located, load it as a style sheet addition - QString directoryPath = QFileInfo(app->getSettings()->fileName()).absolutePath(); - QString fullPath = QDir(directoryPath).filePath("custom.css"); - if (QFile::exists(fullPath)) { - QFile custom(fullPath); - qInfo() << "Loading stylesheet:" << custom.fileName(); - - custom.open(QFile::ReadOnly); - sheet += custom.readAll(); - custom.close(); - } - + bool hasCustomStyleSheet = false; + { + AppearanceTrace::Scope readTrace(QStringLiteral("stylesheet-read")); + QFile f(":/stylesheets/npp.css"); + qInfo() << "Loading stylesheet:" << f.fileName(); + + f.open(QFile::ReadOnly); + sheet = f.readAll(); + f.close(); + + // If there is a "custom.css" file where the ini is located, load it as a style sheet addition + QString directoryPath = QFileInfo(app->getSettings()->fileName()).absolutePath(); + QString fullPath = QDir(directoryPath).filePath("custom.css"); + if (QFile::exists(fullPath)) { + hasCustomStyleSheet = true; + QFile custom(fullPath); + qInfo() << "Loading stylesheet:" << custom.fileName(); + + custom.open(QFile::ReadOnly); + sheet += custom.readAll(); + custom.close(); + } + } + + QString traceDetails; + if (AppearanceTrace::enabled()) { + traceDetails = QStringLiteral("widgets=%1 bytes=%2 custom-css=%3") + .arg(findChildren().size()).arg(sheet.size()) + .arg(hasCustomStyleSheet ? QStringLiteral("yes") : QStringLiteral("no")); + } + AppearanceTrace::Scope repolishTrace( + QStringLiteral("stylesheet-repolish"), traceDetails); setStyleSheet(sheet); } diff --git a/src/dialogs/PreferencesDialog.cpp b/src/dialogs/PreferencesDialog.cpp index 4af63b2..d4542a6 100644 --- a/src/dialogs/PreferencesDialog.cpp +++ b/src/dialogs/PreferencesDialog.cpp @@ -18,6 +18,7 @@ #include "PreferencesDialog.h" +#include "AppearanceManager.h" #include "NotepadSharpApplication.h" #include "TranslationManager.h" #include "ui_PreferencesDialog.h" @@ -41,6 +42,22 @@ PreferencesDialog::PreferencesDialog(ApplicationSettings *settings, QWidget *par ui->labelAppRestartIcon->hide(); ui->labelAppRestart->hide(); + ui->comboBoxAppearance->addItem(tr("System"), QStringLiteral("system")); + ui->comboBoxAppearance->addItem(tr("Light"), QStringLiteral("light")); + ui->comboBoxAppearance->addItem(tr("Dark"), QStringLiteral("dark")); + const QString appearance = AppearanceManager::modeToString( + AppearanceManager::modeFromString(settings->appearance())); + ui->comboBoxAppearance->setCurrentIndex(ui->comboBoxAppearance->findData(appearance)); + connect(ui->comboBoxAppearance, QOverload::of(&QComboBox::currentIndexChanged), + this, [=](int index) { + settings->setAppearance(ui->comboBoxAppearance->itemData(index).toString()); + }); + connect(settings, &ApplicationSettings::appearanceChanged, this, [=](const QString &value) { + const QString canonical = AppearanceManager::modeToString( + AppearanceManager::modeFromString(value)); + ui->comboBoxAppearance->setCurrentIndex(ui->comboBoxAppearance->findData(canonical)); + }); + MapSettingToCheckBox(ui->checkBoxMenuBar, &ApplicationSettings::showMenuBar, &ApplicationSettings::setShowMenuBar, &ApplicationSettings::showMenuBarChanged); MapSettingToCheckBox(ui->checkBoxToolBar, &ApplicationSettings::showToolBar, &ApplicationSettings::setShowToolBar, &ApplicationSettings::showToolBarChanged); MapSettingToCheckBox(ui->checkBoxStatusBar, &ApplicationSettings::showStatusBar, &ApplicationSettings::setShowStatusBar, &ApplicationSettings::showStatusBarChanged); diff --git a/src/dialogs/PreferencesDialog.ui b/src/dialogs/PreferencesDialog.ui index 6256995..2586d7e 100644 --- a/src/dialogs/PreferencesDialog.ui +++ b/src/dialogs/PreferencesDialog.ui @@ -28,7 +28,24 @@ 644 - + + + + + + + Appearance: + + + comboBoxAppearance + + + + + + + + diff --git a/src/docks/LuaConsoleDock.cpp b/src/docks/LuaConsoleDock.cpp index a9acefb..ed430f8 100644 --- a/src/docks/LuaConsoleDock.cpp +++ b/src/docks/LuaConsoleDock.cpp @@ -20,6 +20,8 @@ #include "LuaConsoleDock.h" #include "ui_LuaConsoleDock.h" +#include "AppearanceManager.h" +#include "AppearanceTrace.h" #include "ScintillaNext.h" #include "ILexer.h" #include "Lexilla.h" @@ -83,9 +85,11 @@ static int cf_global_print(lua_State *L) { } -LuaConsoleDock::LuaConsoleDock(LuaState *l, QWidget *parent) : +LuaConsoleDock::LuaConsoleDock(LuaState *l, AppearanceManager *appearanceManager, + QWidget *parent) : QDockWidget(parent), - ui(new Ui::LuaConsoleDock) + ui(new Ui::LuaConsoleDock), + appearanceManager(appearanceManager) { L = l; @@ -182,8 +186,11 @@ LuaConsoleDock::LuaConsoleDock(LuaState *l, QWidget *parent) : setupStyle(input); setupStyle(output); - - output->styleSetFore(39, 0x0000FF); // For error messages + connect(appearanceManager, &AppearanceManager::effectiveAppearanceChanged, this, [this]() { + AppearanceTrace::Scope trace(QStringLiteral("lua-console")); + setupStyle(input); + setupStyle(output); + }); input->setExtraAscent(2); input->setExtraDescent(2); @@ -377,10 +384,13 @@ bool LuaConsoleDock::eventFilter(QObject *obj, QEvent *event) void LuaConsoleDock::setupStyle(ScintillaNext *editor) { + const AppearanceTokens &tokens = appearanceManager->tokens(); + const auto color = &AppearanceManager::scintillaColor; + editor->setEOLMode(SC_EOL_CRLF); - editor->styleSetFore(STYLE_DEFAULT, 0x000000); - editor->styleSetBack(STYLE_DEFAULT, 0xFFFFFF); + editor->styleSetFore(STYLE_DEFAULT, color(tokens.textEditor)); + editor->styleSetBack(STYLE_DEFAULT, color(tokens.surfaceEditor)); editor->styleSetFont(STYLE_DEFAULT, "Courier New"); editor->styleSetSize(STYLE_DEFAULT, 10); editor->styleClearAll(); @@ -392,29 +402,35 @@ void LuaConsoleDock::setupStyle(ScintillaNext *editor) editor->setMarginWidthN(4, 0); editor->setCodePage(SC_CP_UTF8); - editor->styleSetFore(SCE_LUA_COMMENT, 0x008000); - editor->styleSetFore(SCE_LUA_COMMENTLINE, 0x008000); - editor->styleSetFore(SCE_LUA_COMMENTDOC, 0x808000); - editor->styleSetFore(SCE_LUA_LITERALSTRING, 0x4A0095); - editor->styleSetFore(SCE_LUA_PREPROCESSOR, 0x004080); // Technically not used since this is lua 5+ - editor->styleSetFore(SCE_LUA_WORD, 0xFF0000); + editor->styleSetFore(SCE_LUA_COMMENT, color(tokens.syntaxComment)); + editor->styleSetFore(SCE_LUA_COMMENTLINE, color(tokens.syntaxComment)); + editor->styleSetFore(SCE_LUA_COMMENTDOC, color(tokens.syntaxComment)); + editor->styleSetFore(SCE_LUA_LITERALSTRING, color(tokens.syntaxString)); + editor->styleSetFore(SCE_LUA_PREPROCESSOR, color(tokens.syntaxConstant)); + editor->styleSetFore(SCE_LUA_WORD, color(tokens.syntaxKeyword)); editor->styleSetBold(SCE_LUA_WORD, 1); // for SCI_SETKEYWORDS, 0 - editor->styleSetFore(SCE_LUA_NUMBER, 0x0080FF); - editor->styleSetFore(SCE_LUA_STRING, 0x808080); - editor->styleSetFore(SCE_LUA_CHARACTER, 0x808080); - editor->styleSetFore(SCE_LUA_OPERATOR, 0x800000); + editor->styleSetFore(SCE_LUA_NUMBER, color(tokens.syntaxNumber)); + editor->styleSetFore(SCE_LUA_STRING, color(tokens.syntaxString)); + editor->styleSetFore(SCE_LUA_CHARACTER, color(tokens.syntaxString)); + editor->styleSetFore(SCE_LUA_OPERATOR, color(tokens.textEditor)); editor->styleSetBold(SCE_LUA_OPERATOR, 1); - editor->styleSetFore(SCE_LUA_WORD2, 0xC08000); + editor->styleSetFore(SCE_LUA_WORD2, color(tokens.syntaxFunction)); editor->styleSetBold(SCE_LUA_WORD2, 1); // for SCI_SETKEYWORDS, 1 - editor->styleSetFore(SCE_LUA_WORD3, 0xFF0080); + editor->styleSetFore(SCE_LUA_WORD3, color(tokens.syntaxControlFlow)); editor->styleSetBold(SCE_LUA_WORD3, 1); // for SCI_SETKEYWORDS, 2 - editor->styleSetFore(SCE_LUA_WORD4, 0xA00000); + editor->styleSetFore(SCE_LUA_WORD4, color(tokens.syntaxType)); editor->styleSetBold(SCE_LUA_WORD4, 1); editor->styleSetItalic(SCE_LUA_WORD4, 1); // for SCI_SETKEYWORDS, 3 - editor->styleSetFore(SCE_LUA_LABEL, 0x008080); + editor->styleSetFore(SCE_LUA_LABEL, color(tokens.syntaxConstant)); editor->styleSetBold(SCE_LUA_LABEL, 1); - editor->styleSetFore(SCE_LUA_WORD5, 0x004080); // for SCI_SETKEYWORDS, 4, Scintilla defines + editor->styleSetFore(SCE_LUA_WORD5, color(tokens.syntaxAttribute)); editor->styleSetBold(SCE_LUA_WORD5, 1); - editor->styleSetFore(SCE_LUA_WORD6, 0x004080); // for SCI_SETKEYWORDS, 5, Notepad++ defines + editor->styleSetFore(SCE_LUA_WORD6, color(tokens.syntaxAttribute)); editor->styleSetBold(SCE_LUA_WORD6, 1); + editor->styleSetFore(STYLE_LINENUMBER, color(tokens.textSecondary)); + editor->styleSetBack(STYLE_LINENUMBER, color(tokens.surfaceShell)); + editor->styleSetFore(39, color(tokens.stateError)); + editor->setCaretFore(color(tokens.editorCaret)); + editor->setElementColour(SC_ELEMENT_SELECTION_BACK, + AppearanceManager::scintillaElementColor(tokens.selectionActive)); } diff --git a/src/docks/LuaConsoleDock.h b/src/docks/LuaConsoleDock.h index 2707c3f..810b5ad 100644 --- a/src/docks/LuaConsoleDock.h +++ b/src/docks/LuaConsoleDock.h @@ -24,6 +24,7 @@ class ScintillaNext; class LuaState; +class AppearanceManager; namespace Ui { class LuaConsoleDock; @@ -34,7 +35,8 @@ class LuaConsoleDock : public QDockWidget Q_OBJECT public: - explicit LuaConsoleDock(LuaState *l, QWidget *parent = 0); + explicit LuaConsoleDock(LuaState *l, AppearanceManager *appearanceManager, + QWidget *parent = 0); ~LuaConsoleDock(); void writeToOutput(const char *s); @@ -54,6 +56,7 @@ public slots: private: Ui::LuaConsoleDock *ui; + AppearanceManager *appearanceManager; ScintillaNext *output; ScintillaNext *input; diff --git a/src/docks/SearchResultsDock.cpp b/src/docks/SearchResultsDock.cpp index 866a612..3aa642e 100644 --- a/src/docks/SearchResultsDock.cpp +++ b/src/docks/SearchResultsDock.cpp @@ -18,6 +18,8 @@ #include "ApplicationSettings.h" +#include "AppearanceManager.h" +#include "AppearanceTrace.h" #include "NotepadSharpApplication.h" #include "SearchResultHighlighterDelegate.h" #include "SearchResultData.h" @@ -32,9 +34,10 @@ #include -SearchResultsDock::SearchResultsDock(QWidget *parent) : +SearchResultsDock::SearchResultsDock(AppearanceManager *appearanceManager, QWidget *parent) : QDockWidget(parent), - ui(new Ui::SearchResultsDock) + ui(new Ui::SearchResultsDock), + appearanceManager(appearanceManager) { ui->setupUi(this); @@ -66,7 +69,10 @@ SearchResultsDock::SearchResultsDock(QWidget *parent) : menu.exec(QCursor::pos()); }); - ui->treeWidget->setItemDelegate(new SearchResultHighlighterDelegate(ui->treeWidget)); + ui->treeWidget->setItemDelegate( + new SearchResultHighlighterDelegate(appearanceManager, ui->treeWidget)); + connect(appearanceManager, &AppearanceManager::effectiveAppearanceChanged, + this, &SearchResultsDock::applyAppearance); ApplicationSettings *settings = qobject_cast(qApp)->getSettings(); auto updateTreeWidgetFont = [=, this]() { @@ -99,8 +105,7 @@ void SearchResultsDock::newSearch(const QString searchTerm) currentSearch = new QTreeWidgetItem(); ui->treeWidget->insertTopLevelItem(0, currentSearch); - currentSearch->setBackground(0, QColor(232, 232, 255)); - currentSearch->setForeground(0, QColor(0, 0, 170)); + applyItemAppearance(currentSearch); currentSearch->setExpanded(true); currentSearch->setFirstColumnSpanned(true); @@ -118,8 +123,7 @@ void SearchResultsDock::newFileEntry(ScintillaNext *editor) currentFile = new QTreeWidgetItem(currentSearch); currentFile->setData(0, Qt::UserRole, QVariant::fromValue(editor_pointer)); - currentFile->setBackground(0, QColor(213, 255, 213)); - currentFile->setForeground(0, QColor(0, 128, 0)); + applyItemAppearance(currentFile); currentFile->setExpanded(true); currentFile->setFirstColumnSpanned(true); @@ -133,7 +137,7 @@ void SearchResultsDock::newResultsEntry(const QString line, int lineNumber, int // Scintilla internally references line numbers starting at 0, however it needs displayed starting at 1 item->setText(0, QString::number(lineNumber + 1)); - item->setBackground(0, QBrush(QColor(220, 220, 220))); + applyItemAppearance(item); item->setTextAlignment(0, Qt::AlignRight); item->setData(1, SearchResultData::LineNumber, lineNumber); @@ -160,6 +164,38 @@ void SearchResultsDock::completeSearch() ui->treeWidget->resizeColumnToContents(1); } +void SearchResultsDock::applyAppearance() +{ + AppearanceTrace::Scope trace(QStringLiteral("search-results"), + QStringLiteral("root-items=%1").arg(ui->treeWidget->topLevelItemCount())); + for (int index = 0; index < ui->treeWidget->topLevelItemCount(); ++index) + applyItemAppearance(ui->treeWidget->topLevelItem(index)); + ui->treeWidget->viewport()->update(); +} + +void SearchResultsDock::applyItemAppearance(QTreeWidgetItem *item) const +{ + if (!item) + return; + + const AppearanceTokens &tokens = appearanceManager->tokens(); + if (!item->parent()) { + item->setBackground(0, tokens.surfaceRaised); + item->setForeground(0, tokens.textPrimary); + } + else if (!item->parent()->parent()) { + item->setBackground(0, tokens.selectionInactive); + item->setForeground(0, tokens.stateSuccess); + } + else { + item->setBackground(0, tokens.surfaceShell); + item->setForeground(0, tokens.textSecondary); + } + + for (int index = 0; index < item->childCount(); ++index) + applyItemAppearance(item->child(index)); +} + void SearchResultsDock::collapseAll() const { ui->treeWidget->collapseAll(); diff --git a/src/docks/SearchResultsDock.h b/src/docks/SearchResultsDock.h index 13076d9..060be33 100644 --- a/src/docks/SearchResultsDock.h +++ b/src/docks/SearchResultsDock.h @@ -29,6 +29,7 @@ class SearchResultsDock; } class QTreeWidgetItem; +class AppearanceManager; class ScintillaNext; class SearchResultsDock : public QDockWidget, public ISearchResultsHandler @@ -36,7 +37,7 @@ class SearchResultsDock : public QDockWidget, public ISearchResultsHandler Q_OBJECT public: - explicit SearchResultsDock(QWidget *parent = nullptr); + explicit SearchResultsDock(AppearanceManager *appearanceManager, QWidget *parent = nullptr); ~SearchResultsDock(); void newSearch(const QString searchTerm) override; @@ -61,8 +62,11 @@ private slots: void searchResultActivated(ScintillaNext *editor, int lineNumber, int startPositionFromBeginning, int endPositionFromBeginning); private: + void applyAppearance(); + void applyItemAppearance(QTreeWidgetItem *item) const; void updateSearchStatus(); Ui::SearchResultsDock *ui; + AppearanceManager *appearanceManager; QString searchTerm; QString currentFilePath; diff --git a/src/main.cpp b/src/main.cpp index 549a9b1..0c797af 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -42,6 +42,10 @@ int main(int argc, char *argv[]) // Default settings format QSettings::setDefaultFormat(QSettings::IniFormat); + const QString configHome = qEnvironmentVariable("NOTEPADSHARP_CONFIG_HOME"); + if (!configHome.isEmpty()) { + QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, configHome); + } NotepadSharpApplication app(argc, argv); diff --git a/src/scripts/init.lua b/src/scripts/init.lua index 2d14bb9..2aa5ef8 100644 --- a/src/scripts/init.lua +++ b/src/scripts/init.lua @@ -2,6 +2,28 @@ function rgb(x) return ((x & 0xFF) << 16) | (x & 0xFF00) | ((x & 0xFF0000) >> 16) end +function UpdateTheme() + theme = { + dark = theme_dark_mode, + default_fg = theme_default_fg, + default_bg = theme_default_bg, + comment = theme_comment, + string = theme_string, + number = theme_number, + keyword = theme_keyword, + control_flow = theme_control_flow, + func = theme_function, + type = theme_type, + variable = theme_variable, + constant = theme_constant, + tag = theme_tag, + attribute = theme_attribute, + error = theme_error, + } +end + +UpdateTheme() + function DetectLanguageFromContents(contents) for name, L in pairs(languages) do if L.first_line then @@ -48,11 +70,63 @@ function DialogFilters() return table.concat(filters, ";;") end -function SetStyle(L) +local function themedForeground(styleName, fallback) + if not theme.dark then return fallback end + + local name = string.upper(tostring(styleName)) + if string.find(name, "ERROR", 1, true) or string.find(name, "ILLEGAL", 1, true) + or string.find(name, "UNKNOWN", 1, true) then + return theme.error + end + if string.find(name, "COMMENT", 1, true) then return theme.comment end + if string.find(name, "STRING", 1, true) or string.find(name, "CHARACTER", 1, true) + or string.find(name, "VERBATIM", 1, true) or string.find(name, "REGEX", 1, true) then + return theme.string + end + if string.find(name, "NUMBER", 1, true) or string.find(name, "NUMERIC", 1, true) + or string.find(name, "HEX", 1, true) or string.find(name, "BIN", 1, true) then + return theme.number + end + if string.find(name, "FUNCTION", 1, true) or string.find(name, "METHOD", 1, true) + or string.find(name, "DEFNAME", 1, true) or string.find(name, "CMDLET", 1, true) then + return theme.func + end + if string.find(name, "CLASS", 1, true) or string.find(name, "TYPE", 1, true) then + return theme.type + end + if string.find(name, "ATTRIBUTE", 1, true) or string.find(name, "PROPERTY", 1, true) then + return theme.attribute + end + if string.find(name, "TAG", 1, true) then return theme.tag end + if string.find(name, "VARIABLE", 1, true) or string.find(name, "PARAM", 1, true) then + return theme.variable + end + if string.find(name, "CONSTANT", 1, true) or string.find(name, "MACRO", 1, true) then + return theme.constant + end + if string.find(name, "LABEL", 1, true) or string.find(name, "PREPROCESSOR", 1, true) then + return theme.constant + end + if string.find(name, "CONTROL", 1, true) then return theme.control_flow end + if string.find(name, "KEYWORD", 1, true) or string.find(name, "INSTRUCTION", 1, true) + or string.find(name, "COMMAND", 1, true) or string.find(name, "DIRECTIVE", 1, true) + or string.find(name, "RESERVED", 1, true) then + return theme.keyword + end + if string.find(name, "DEFAULT", 1, true) or string.find(name, "WHITE", 1, true) + or string.find(name, "IDENTIFIER", 1, true) or string.find(name, "OPERATOR", 1, true) + or string.find(name, "PUNCTUATION", 1, true) then + return theme.default_fg + end + if fallback == rgb(0x000000) then return theme.default_fg end + return fallback +end + +local function SetStyleAppearance(L) if L.styles then - for _, style in pairs(L.styles) do - editor.StyleFore[style.id] = style.fgColor - editor.StyleBack[style.id] = style.bgColor + for styleName, style in pairs(L.styles) do + editor.StyleFore[style.id] = themedForeground(styleName, style.fgColor) + editor.StyleBack[style.id] = theme.dark and theme.default_bg or style.bgColor if style.fontStyle then editor.StyleBold[style.id] = (style.fontStyle & 1 == 1) @@ -62,6 +136,10 @@ function SetStyle(L) end end end +end + +function SetStyle(L) + SetStyleAppearance(L) if L.keywords then for id, kw in pairs(L.keywords) do @@ -76,6 +154,18 @@ function SetStyle(L) end end +function SetLanguageAppearance(languageName) + local L = languages[languageName] + + SetStyleAppearance(L) + + if L.additionalLanguages then + for _, language in pairs(L.additionalLanguages) do + SetStyleAppearance(languages[language]) + end + end +end + function SetLanguage(languageName) local L = languages[languageName] diff --git a/src/stylesheets/npp.css b/src/stylesheets/npp.css index cc0aa0c..f7ddef5 100644 --- a/src/stylesheets/npp.css +++ b/src/stylesheets/npp.css @@ -1,17 +1,90 @@ QStatusBar { - border-top: 1px solid darkgray; + border-top: 1px solid palette(mid); +} + +QMenuBar, QMenu { + background: palette(window); + color: palette(window-text); +} + +QMenuBar::item:selected, QMenu::item:selected { + background: palette(highlight); + color: palette(highlighted-text); +} + +QMenu::separator { + background: palette(mid); +} + +QToolTip { + background: palette(tool-tip-base); + color: palette(tool-tip-text); + border: 1px solid palette(mid); +} + +QLineEdit, QComboBox, QSpinBox, QDoubleSpinBox, QTextEdit, QPlainTextEdit, +QListView, QTreeView, QTableView { + background: palette(base); + color: palette(text); + border: 1px solid palette(mid); + selection-background-color: palette(highlight); + selection-color: palette(highlighted-text); +} + +QComboBox::drop-down, QSpinBox::up-button, QSpinBox::down-button, +QDoubleSpinBox::up-button, QDoubleSpinBox::down-button { + background: palette(button); + border-left: 1px solid palette(mid); +} + +QComboBox QAbstractItemView { + background: palette(base); + color: palette(text); + selection-background-color: palette(highlight); + selection-color: palette(highlighted-text); +} + +QPushButton { + background: palette(button); + color: palette(button-text); + border: 1px solid palette(mid); +} + +QPushButton:hover { + background: palette(alternate-base); +} + +QPushButton:pressed { + background: palette(highlight); + color: palette(highlighted-text); +} + +QCheckBox, QRadioButton, QGroupBox, QLabel { + color: palette(window-text); +} + +QCheckBox:disabled, QRadioButton:disabled, QGroupBox:disabled, QLabel:disabled, +QLineEdit:disabled, QComboBox:disabled, QSpinBox:disabled, +QDoubleSpinBox:disabled, QPushButton:disabled { + color: palette(placeholder-text); +} + +QHeaderView::section { + background: palette(button); + color: palette(button-text); + border: 1px solid palette(mid); } ads--CDockWidgetTab ads--CElidingLabel { - color : black; + color: palette(window-text); } ads--CDockWidgetTab { - background: rgb(192, 192, 192); + background: palette(window); margin-top: 2px; - border-left: 1px solid white; - border-top: 2px solid white; - border-right: 2px solid gray; + border-left: 1px solid palette(midlight); + border-top: 2px solid palette(midlight); + border-right: 2px solid palette(mid); padding: 2px 2px 0px 2px; border-top-left-radius: 2px; border-top-right-radius: 2px; @@ -19,38 +92,38 @@ ads--CDockWidgetTab { ads--CDockWidgetTab[activeTab="true"] { - background: rgb(240, 240, 240); - border-top: 4px solid rgb(255, 202, 176); + background: palette(base); + border-top: 4px solid palette(highlight); padding-bottom: 2px; margin-top: 1px; } ads--CDockWidgetTab[focused="true"] { - background: rgb(240, 240, 240); - border-top: 4px solid orange; + background: palette(base); + border-top: 4px solid palette(highlight); padding-bottom: 2px; margin-top: 1px; } ads--CDockWidgetTab:hover[activeTab="false"] { - background: rgb(210, 210, 210); + background: palette(alternate-base); } ads--CDockAreaTitleBar { background: transparent; - border-bottom: 1px solid darkgray; + border-bottom: 1px solid palette(mid); padding-bottom: 0px; } ads--CDockWidgetTab[activeTab="false"] QLabel { - color: gray; + color: palette(placeholder-text); } #tabCloseButton { - background: rgba(0, 0, 0, 16); + background: transparent; margin-bottom: 1px; margin-left: 4px; border: none; @@ -59,19 +132,47 @@ ads--CDockWidgetTab[activeTab="false"] QLabel { #tabCloseButton:hover { - border: 1px solid rgba(0, 0, 0, 32); - background-color: rgb(251, 115, 115); - color: green; + border: 1px solid palette(mid); + background-color: palette(alternate-base); + color: palette(window-text); } #tabCloseButton:pressed { - background: red; + background: palette(highlight); } #QuickFindWidget { - border-left: 1px solid darkgray; - border-right: 1px solid darkgray; - border-bottom: 3px solid blue; + border-left: 1px solid palette(mid); + border-right: 1px solid palette(mid); + border-bottom: 3px solid palette(highlight); background: palette(window); } + +QScrollBar:horizontal, QScrollBar:vertical { + background: palette(window); + border: none; +} + +QScrollBar::handle:horizontal, QScrollBar::handle:vertical { + background: palette(mid); + min-width: 28px; + min-height: 28px; +} + +QScrollBar::handle:hover { + background: palette(placeholder-text); +} + +QScrollBar::add-line, QScrollBar::sub-line { + background: palette(button); + border: 1px solid palette(mid); +} + +QScrollBar::add-page, QScrollBar::sub-page { + background: transparent; +} + +QLineEdit[validationError="true"] { + border: 2px solid palette(bright-text); +} diff --git a/src/widgets/FadingIndicator.cpp b/src/widgets/FadingIndicator.cpp index 143aafa..b0a90a1 100644 --- a/src/widgets/FadingIndicator.cpp +++ b/src/widgets/FadingIndicator.cpp @@ -48,9 +48,7 @@ class FadingIndicatorPrivate : public QWidget QFont font = m_label->font(); font.setPixelSize(size == FadingIndicator::LargeText ? 30 : 18); m_label->setFont(font); - QPalette pal = palette(); - pal.setColor(QPalette::WindowText, pal.color(QPalette::Window)); - m_label->setPalette(pal); + m_label->setForegroundRole(QPalette::ToolTipText); auto layout = new QHBoxLayout; setLayout(layout); layout->addWidget(m_label); @@ -101,8 +99,8 @@ class FadingIndicatorPrivate : public QWidget if (!m_pixmap.isNull()) { p.drawPixmap(rect(), m_pixmap); } else { - p.setBrush(palette().color(QPalette::WindowText)); - p.setPen(Qt::NoPen); + p.setBrush(palette().color(QPalette::ToolTipBase)); + p.setPen(palette().color(QPalette::Mid)); p.drawRoundedRect(rect(), 15, 15); } } diff --git a/src/widgets/QuickFindWidget.cpp b/src/widgets/QuickFindWidget.cpp index 22408a1..0bacf3d 100644 --- a/src/widgets/QuickFindWidget.cpp +++ b/src/widgets/QuickFindWidget.cpp @@ -18,6 +18,8 @@ #include "FocusWatcher.h" +#include "AppearanceManager.h" +#include "AppearanceTrace.h" #include "QuickFindWidget.h" #include "ScintillaNext.h" #include "FadingIndicator.h" @@ -28,9 +30,10 @@ #include #include -QuickFindWidget::QuickFindWidget(QWidget *parent) : +QuickFindWidget::QuickFindWidget(AppearanceManager *appearanceManager, QWidget *parent) : QFrame(parent), - ui(new Ui::QuickFindWidget) + ui(new Ui::QuickFindWidget), + appearanceManager(appearanceManager) { ui->setupUi(this); @@ -50,6 +53,8 @@ QuickFindWidget::QuickFindWidget(QWidget *parent) : connect(ui->buttonMatchCase, &QToolButton::toggled, this, &QuickFindWidget::performNewSearch); connect(ui->buttonWholeWord, &QToolButton::toggled, this, &QuickFindWidget::performNewSearch); connect(ui->buttonRegexp, &QToolButton::toggled, this, &QuickFindWidget::performNewSearch); + connect(appearanceManager, &AppearanceManager::effectiveAppearanceChanged, + this, &QuickFindWidget::applyAppearance); } QuickFindWidget::~QuickFindWidget() @@ -103,12 +108,12 @@ bool QuickFindWidget::eventFilter(QObject *obj, QEvent *event) void QuickFindWidget::setSearchContextColorBad() { - setSearchContextColor(QStringLiteral("red")); + setSearchContextColor(appearanceManager->tokens().stateError); } void QuickFindWidget::setSearchContextColorGood() { - setSearchContextColor(QStringLiteral("blue")); + setSearchContextColor(appearanceManager->tokens().accentPrimary); } void QuickFindWidget::performNewSearch() @@ -257,22 +262,39 @@ int QuickFindWidget::computeSearchFlags() const return searchFlags; } -void QuickFindWidget::setSearchContextColor(const QString &color) +void QuickFindWidget::setSearchContextColor(const QColor &color) { - ui->lineEdit->setStyleSheet(QStringLiteral("border: 1px solid %1; padding: 2px;").arg(color)); + ui->lineEdit->setStyleSheet(QStringLiteral("border: 1px solid %1; padding: 2px;") + .arg(color.name(QColor::HexRgb))); } void QuickFindWidget::initializeEditorIndicator() { indicator = editor->allocateIndicator(QStringLiteral("quick_find")); - editor->indicSetFore(indicator, 0xFF8000); + editor->indicSetFore(indicator, + AppearanceManager::scintillaColor(appearanceManager->tokens().accentHover)); editor->indicSetStyle(indicator, INDIC_FULLBOX); editor->indicSetOutlineAlpha(indicator, 150); editor->indicSetAlpha(indicator, 50); editor->indicSetUnder(indicator, true); } +void QuickFindWidget::applyAppearance() +{ + AppearanceTrace::Scope trace(QStringLiteral("quick-find"), + QStringLiteral("matches=%1").arg(matches.size())); + if (editor && indicator >= 0) { + editor->indicSetFore( + indicator, AppearanceManager::scintillaColor(appearanceManager->tokens().accentHover)); + } + + if (!searchText().isEmpty() && matches.isEmpty()) + setSearchContextColorBad(); + else + setSearchContextColorGood(); +} + QString QuickFindWidget::searchText() const { return ui->lineEdit->text(); diff --git a/src/widgets/QuickFindWidget.h b/src/widgets/QuickFindWidget.h index a92157b..6a7d11a 100644 --- a/src/widgets/QuickFindWidget.h +++ b/src/widgets/QuickFindWidget.h @@ -25,6 +25,7 @@ #include #include #include +#include #include "Finder.h" #include "ScintillaNext.h" @@ -33,13 +34,14 @@ namespace Ui { class QuickFindWidget; } +class AppearanceManager; class QuickFindWidget : public QFrame { Q_OBJECT public: - explicit QuickFindWidget(QWidget *parent = nullptr); + explicit QuickFindWidget(AppearanceManager *appearanceManager, QWidget *parent = nullptr); ~QuickFindWidget(); void setEditor(ScintillaNext *editor); @@ -69,7 +71,8 @@ private slots: void setSearchContextColorBad(); void setSearchContextColorGood(); - void setSearchContextColor(const QString &color); + void setSearchContextColor(const QColor &color); + void applyAppearance(); void initializeEditorIndicator(); QString searchText() const; @@ -77,9 +80,10 @@ private slots: void showWrapIndicator(); Ui::QuickFindWidget *ui; + AppearanceManager *appearanceManager; ScintillaNext *editor = Q_NULLPTR; Finder *finder = Q_NULLPTR; - int indicator; + int indicator = -1; QList> matches; qsizetype currentMatchIndex = -1; diff --git a/src/widgets/TabsQuickActionsBar.cpp b/src/widgets/TabsQuickActionsBar.cpp index 9e76a83..77be93b 100644 --- a/src/widgets/TabsQuickActionsBar.cpp +++ b/src/widgets/TabsQuickActionsBar.cpp @@ -18,6 +18,7 @@ #include +#include #include #include #include @@ -31,6 +32,21 @@ namespace constexpr QLatin1StringView IconPlusPath(":/icons/plus.svg"); constexpr QLatin1StringView IconListPath(":/icons/list_with_icons.svg"); constexpr QLatin1StringView IconCrossPath(":/icons/cross.svg"); + + QIcon monochromeIcon(QLatin1StringView resource, const QColor &color) + { + const QIcon source(resource); + QIcon result; + for (const int size : {16, 20, 24, 32}) { + QPixmap pixmap = source.pixmap(size, size); + QPainter painter(&pixmap); + painter.setCompositionMode(QPainter::CompositionMode_SourceIn); + painter.fillRect(pixmap.rect(), color); + painter.end(); + result.addPixmap(pixmap); + } + return result; + } } TabsQuickActionsBar::TabsQuickActionsBar(const Buttons &visibileButtons, QWidget *parent) @@ -63,9 +79,25 @@ TabsQuickActionsBar::TabsQuickActionsBar(const Buttons &visibileButtons, QWidget connect(tabsMenu, &QMenu::aboutToShow, this, [this, tabsMenu]() { emit tabsMenuAboutToShow(tabsMenu); }); connect(closeCurrentTabAction, &QAction::triggered, this, &TabsQuickActionsBar::closeCurrentTabClicked); + applyAppearance(); setVisibileButtons(visibileButtons); } +bool TabsQuickActionsBar::event(QEvent *event) +{ + if (event->type() == QEvent::PaletteChange) + applyAppearance(); + return QToolBar::event(event); +} + +void TabsQuickActionsBar::applyAppearance() +{ + const QColor color = palette().color(QPalette::ButtonText); + createNewTabAction->setIcon(monochromeIcon(IconPlusPath, color)); + showTabsMenuAction->setIcon(monochromeIcon(IconListPath, color)); + closeCurrentTabAction->setIcon(monochromeIcon(IconCrossPath, color)); +} + void TabsQuickActionsBar::setVisibileButtons(const Buttons &buttons) { if (visibileButtons == buttons) diff --git a/src/widgets/TabsQuickActionsBar.h b/src/widgets/TabsQuickActionsBar.h index 3dc3862..a8a0286 100644 --- a/src/widgets/TabsQuickActionsBar.h +++ b/src/widgets/TabsQuickActionsBar.h @@ -65,7 +65,11 @@ class TabsQuickActionsBar : public QToolBar void setVisibileButtons(const Buttons &buttons); void setVisibileButton(Button button, bool on = true); +protected: + bool event(QEvent *event) override; + private: + void applyAppearance(); Buttons visibileButtons = TabsQuickActionsBar::All; QAction *createNewTabAction = nullptr; diff --git a/tests/AppearanceManagerTests.cpp b/tests/AppearanceManagerTests.cpp new file mode 100644 index 0000000..2e9d53a --- /dev/null +++ b/tests/AppearanceManagerTests.cpp @@ -0,0 +1,235 @@ +#include +#include +#include +#include +#include + +#include "AppearanceManager.h" +#include "ApplicationSettings.h" + +class AppearanceManagerTests : public QObject +{ + Q_OBJECT + +private slots: + void parsesOnlyCanonicalWireValues(); + void defaultsMissingSettingsToClassicLight(); + void defaultsInvalidSettingsToSystem(); + void persistsExplicitMode(); + void emitsOnlyWhenEffectiveAppearanceChanges(); + void followsSystemChangesOnlyInSystemMode(); + void systemDarkKeepsSemanticSyntax(); + void ignoresPaletteChangesDuringSynchronousRefresh(); + void ignoresWidgetPaletteEventsWhenApplicationPaletteIsUnchanged(); + void refreshesOnceWhenApplicationPaletteChanges(); + void writesOptInPerformanceTrace(); +}; + +void AppearanceManagerTests::parsesOnlyCanonicalWireValues() +{ + QCOMPARE(AppearanceManager::modeFromString(QStringLiteral("system")), AppearanceManager::Mode::System); + QCOMPARE(AppearanceManager::modeFromString(QStringLiteral("light")), AppearanceManager::Mode::Light); + QCOMPARE(AppearanceManager::modeFromString(QStringLiteral("dark")), AppearanceManager::Mode::Dark); + QCOMPARE(AppearanceManager::modeFromString(QStringLiteral("Dark")), AppearanceManager::Mode::System); + QCOMPARE(AppearanceManager::modeFromString(QStringLiteral("2")), AppearanceManager::Mode::System); + QCOMPARE(AppearanceManager::modeFromString(QString()), AppearanceManager::Mode::System); + QCOMPARE(AppearanceManager::scintillaElementColor(QColor(0x27, 0x67, 0x82, 0xDD)), + 0xDD826727u); +} + +void AppearanceManagerTests::defaultsMissingSettingsToClassicLight() +{ + QTemporaryDir directory; + QVERIFY(directory.isValid()); + + ApplicationSettings settings(directory.filePath(QStringLiteral("settings.ini")), QSettings::IniFormat); + QCOMPARE(settings.appearance(), QStringLiteral("light")); + + AppearanceManager manager(&settings, [] { return Qt::ColorScheme::Dark; }); + QCOMPARE(manager.requestedMode(), AppearanceManager::Mode::Light); + QCOMPARE(manager.effectiveAppearance(), AppearanceManager::EffectiveAppearance::Light); +} + +void AppearanceManagerTests::defaultsInvalidSettingsToSystem() +{ + QTemporaryDir directory; + QVERIFY(directory.isValid()); + + ApplicationSettings settings(directory.filePath(QStringLiteral("settings.ini")), QSettings::IniFormat); + settings.setAppearance(QStringLiteral("invalid")); + + AppearanceManager manager(&settings, [] { return Qt::ColorScheme::Light; }); + QCOMPARE(manager.requestedMode(), AppearanceManager::Mode::System); + QCOMPARE(manager.effectiveAppearance(), AppearanceManager::EffectiveAppearance::Light); +} + +void AppearanceManagerTests::persistsExplicitMode() +{ + QTemporaryDir directory; + QVERIFY(directory.isValid()); + const QString settingsPath = directory.filePath(QStringLiteral("settings.ini")); + + { + ApplicationSettings settings(settingsPath, QSettings::IniFormat); + AppearanceManager manager(&settings, [] { return Qt::ColorScheme::Light; }); + manager.setRequestedMode(AppearanceManager::Mode::Dark); + settings.sync(); + } + + ApplicationSettings settings(settingsPath, QSettings::IniFormat); + AppearanceManager manager(&settings, [] { return Qt::ColorScheme::Light; }); + QCOMPARE(settings.appearance(), QStringLiteral("dark")); + QCOMPARE(manager.requestedMode(), AppearanceManager::Mode::Dark); + QCOMPARE(manager.effectiveAppearance(), AppearanceManager::EffectiveAppearance::Dark); +} + +void AppearanceManagerTests::emitsOnlyWhenEffectiveAppearanceChanges() +{ + QTemporaryDir directory; + ApplicationSettings settings(directory.filePath(QStringLiteral("settings.ini")), QSettings::IniFormat); + AppearanceManager manager(&settings, [] { return Qt::ColorScheme::Dark; }); + QSignalSpy spy(&manager, &AppearanceManager::effectiveAppearanceChanged); + + manager.setRequestedMode(AppearanceManager::Mode::Dark); + QCOMPARE(spy.count(), 1); + QCOMPARE(QApplication::palette().color(QPalette::Base), QColor(QStringLiteral("#121314"))); + + manager.setRequestedMode(AppearanceManager::Mode::Light); + QCOMPARE(spy.count(), 2); + QCOMPARE(manager.effectiveAppearance(), AppearanceManager::EffectiveAppearance::Light); +} + +void AppearanceManagerTests::followsSystemChangesOnlyInSystemMode() +{ + QTemporaryDir directory; + ApplicationSettings settings(directory.filePath(QStringLiteral("settings.ini")), QSettings::IniFormat); + settings.setAppearance(QStringLiteral("system")); + Qt::ColorScheme scheme = Qt::ColorScheme::Light; + AppearanceManager manager(&settings, [&scheme] { return scheme; }); + QSignalSpy spy(&manager, &AppearanceManager::effectiveAppearanceChanged); + + scheme = Qt::ColorScheme::Dark; + manager.refreshSystemAppearance(); + QCOMPARE(spy.count(), 1); + QCOMPARE(manager.effectiveAppearance(), AppearanceManager::EffectiveAppearance::Dark); + + manager.setRequestedMode(AppearanceManager::Mode::Light); + QCOMPARE(spy.count(), 2); + scheme = Qt::ColorScheme::Dark; + manager.refreshSystemAppearance(); + QCOMPARE(spy.count(), 2); +} + +void AppearanceManagerTests::systemDarkKeepsSemanticSyntax() +{ + QTemporaryDir directory; + ApplicationSettings settings(directory.filePath(QStringLiteral("settings.ini")), QSettings::IniFormat); + settings.setAppearance(QStringLiteral("system")); + AppearanceManager manager(&settings, [] { return Qt::ColorScheme::Dark; }); + + QCOMPARE(manager.effectiveAppearance(), AppearanceManager::EffectiveAppearance::Dark); + QVERIFY(manager.tokens().syntaxKeyword != manager.tokens().textEditor); + QVERIFY(manager.tokens().syntaxString != manager.tokens().syntaxKeyword); + QVERIFY(manager.tokens().diffAddedMarker != manager.tokens().diffDeletedMarker); +} + +void AppearanceManagerTests::ignoresPaletteChangesDuringSynchronousRefresh() +{ + QTemporaryDir directory; + ApplicationSettings settings(directory.filePath(QStringLiteral("settings.ini")), QSettings::IniFormat); + AppearanceManager manager(&settings, [] { return Qt::ColorScheme::Light; }); + QSignalSpy spy(&manager, &AppearanceManager::effectiveAppearanceChanged); + int refreshCount = 0; + + connect(&manager, &AppearanceManager::effectiveAppearanceChanged, qApp, [&]() { + if (++refreshCount < 4) { + QEvent paletteChange(QEvent::ApplicationPaletteChange); + QCoreApplication::sendEvent(qApp, &paletteChange); + } + }); + + manager.setRequestedMode(AppearanceManager::Mode::System); + + QCOMPARE(spy.count(), 1); + QCOMPARE(refreshCount, 1); + QCOMPARE(manager.requestedMode(), AppearanceManager::Mode::System); + QCOMPARE(manager.effectiveAppearance(), AppearanceManager::EffectiveAppearance::Light); +} + +void AppearanceManagerTests::ignoresWidgetPaletteEventsWhenApplicationPaletteIsUnchanged() +{ + QTemporaryDir directory; + ApplicationSettings settings(directory.filePath(QStringLiteral("settings.ini")), QSettings::IniFormat); + settings.setAppearance(QStringLiteral("system")); + AppearanceManager manager(&settings, [] { return Qt::ColorScheme::Light; }); + QSignalSpy spy(&manager, &AppearanceManager::effectiveAppearanceChanged); + QWidget widget; + + for (int index = 0; index < 5; ++index) { + QEvent paletteChange(QEvent::ApplicationPaletteChange); + QCoreApplication::sendEvent(&widget, &paletteChange); + } + + QCOMPARE(spy.count(), 0); +} + +void AppearanceManagerTests::refreshesOnceWhenApplicationPaletteChanges() +{ + QTemporaryDir directory; + ApplicationSettings settings(directory.filePath(QStringLiteral("settings.ini")), QSettings::IniFormat); + settings.setAppearance(QStringLiteral("system")); + AppearanceManager manager(&settings, [] { return Qt::ColorScheme::Light; }); + QSignalSpy spy(&manager, &AppearanceManager::effectiveAppearanceChanged); + QWidget widget; + widget.show(); + + const QPalette originalPalette = QApplication::palette(); + QPalette changedPalette = originalPalette; + const QColor changedBase = originalPalette.color(QPalette::Base) == Qt::red + ? QColor(Qt::blue) + : QColor(Qt::red); + changedPalette.setColor(QPalette::Base, changedBase); + QApplication::setPalette(changedPalette); + + QCOMPARE(spy.count(), 1); + QCOMPARE(manager.tokens().surfaceEditor, changedBase); + + QApplication::setPalette(originalPalette); + QCOMPARE(spy.count(), 2); +} + +void AppearanceManagerTests::writesOptInPerformanceTrace() +{ + QTemporaryDir directory; + QVERIFY(directory.isValid()); + const QString tracePath = directory.filePath(QStringLiteral("appearance-trace.log")); + const QByteArray previousTracePath = qgetenv("NOTEPADSHARP_APPEARANCE_TRACE"); + qputenv("NOTEPADSHARP_APPEARANCE_TRACE", tracePath.toUtf8()); + + ApplicationSettings settings(directory.filePath(QStringLiteral("settings.ini")), QSettings::IniFormat); + AppearanceManager manager(&settings, [] { return Qt::ColorScheme::Light; }); + manager.setRequestedMode(AppearanceManager::Mode::Dark); + manager.setRequestedMode(AppearanceManager::Mode::System); + + QFile traceFile(tracePath); + const bool opened = traceFile.open(QIODevice::ReadOnly | QIODevice::Text); + const QString trace = opened ? QString::fromUtf8(traceFile.readAll()) : QString(); + if (previousTracePath.isNull()) + qunsetenv("NOTEPADSHARP_APPEARANCE_TRACE"); + else + qputenv("NOTEPADSHARP_APPEARANCE_TRACE", previousTracePath); + + QVERIFY(opened); + QVERIFY(trace.contains(QStringLiteral("trace-enabled"))); + QVERIFY(trace.contains(QStringLiteral("begin trigger=setting-change requested=dark"))); + QVERIFY(trace.contains(QStringLiteral("begin trigger=setting-change requested=system"))); + QVERIFY(trace.contains(QStringLiteral("component=restore-system-palette"))); + QVERIFY(trace.contains(QStringLiteral("component=application-palette"))); + QVERIFY(trace.contains(QStringLiteral("component=native-windows"))); + QVERIFY(trace.contains(QStringLiteral("component=synchronous-slots"))); + QVERIFY(trace.contains(QStringLiteral(" end elapsed-ms="))); +} + +QTEST_MAIN(AppearanceManagerTests) + +#include "AppearanceManagerTests.moc" \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c543bc3..3c4aa13 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,3 +1,67 @@ +qt_add_executable(AppearanceManagerTests + AppearanceManagerTests.cpp + ${CMAKE_SOURCE_DIR}/src/AppearanceManager.cpp + ${CMAKE_SOURCE_DIR}/src/AppearanceManager.h + ${CMAKE_SOURCE_DIR}/src/ApplicationSettings.cpp + ${CMAKE_SOURCE_DIR}/src/ApplicationSettings.h + ${CMAKE_SOURCE_DIR}/src/WindowsAppearance.cpp + ${CMAKE_SOURCE_DIR}/src/WindowsAppearance.h +) + +target_include_directories(AppearanceManagerTests PRIVATE ${CMAKE_SOURCE_DIR}/src) + +target_link_libraries(AppearanceManagerTests PRIVATE + Qt6::Core + Qt6::Gui + Qt6::Test + Qt6::Widgets +) + +if(WIN32) + target_link_libraries(AppearanceManagerTests PRIVATE dwmapi) +endif() + +add_test(NAME AppearanceManagerTests COMMAND AppearanceManagerTests) +set_tests_properties(AppearanceManagerTests PROPERTIES + ENVIRONMENT "QT_QPA_PLATFORM=offscreen" + TIMEOUT 30 +) + +qt_add_executable(EditorAppearanceTests + EditorAppearanceTests.cpp + ${CMAKE_SOURCE_DIR}/src/AppearanceManager.cpp + ${CMAKE_SOURCE_DIR}/src/AppearanceManager.h + ${CMAKE_SOURCE_DIR}/src/ApplicationSettings.cpp + ${CMAKE_SOURCE_DIR}/src/ApplicationSettings.h + ${CMAKE_SOURCE_DIR}/src/EditorAppearance.cpp + ${CMAKE_SOURCE_DIR}/src/EditorAppearance.h + ${CMAKE_SOURCE_DIR}/src/QRegexSearch.cpp + ${CMAKE_SOURCE_DIR}/src/QRegexSearch.h + ${CMAKE_SOURCE_DIR}/src/WindowsAppearance.cpp + ${CMAKE_SOURCE_DIR}/src/WindowsAppearance.h +) + +target_include_directories(EditorAppearanceTests PRIVATE ${CMAKE_SOURCE_DIR}/src) + +target_link_libraries(EditorAppearanceTests PRIVATE + Qt6::Core + Qt6::Core5Compat + Qt6::Gui + Qt6::Test + Qt6::Widgets + scintilla +) + +if(WIN32) + target_link_libraries(EditorAppearanceTests PRIVATE dwmapi) +endif() + +add_test(NAME EditorAppearanceTests COMMAND EditorAppearanceTests) +set_tests_properties(EditorAppearanceTests PROPERTIES + ENVIRONMENT "QT_QPA_PLATFORM=offscreen" + TIMEOUT 30 +) + qt_add_executable(CompareEngineTests CompareEngineTests.cpp ${CMAKE_SOURCE_DIR}/src/compare/CompareEngine.cpp diff --git a/tests/EditorAppearanceTests.cpp b/tests/EditorAppearanceTests.cpp new file mode 100644 index 0000000..1eb3ff1 --- /dev/null +++ b/tests/EditorAppearanceTests.cpp @@ -0,0 +1,123 @@ +#include +#include + +#include "AppearanceManager.h" +#include "ApplicationSettings.h" +#include "EditorAppearance.h" + +#include "Scintilla.h" +#include "ScintillaEdit.h" + +class EditorAppearanceTests : public QObject +{ + Q_OBJECT + +private slots: + void appliesDarkTokensAndPreservesDocumentState(); + void restoresNamedStylesAfterStyleClearAll(); + void switchesTheSameEditorBackToLight(); + void preservesExistingLexerStyleAssignments(); +}; + +void EditorAppearanceTests::appliesDarkTokensAndPreservesDocumentState() +{ + QTemporaryDir directory; + QVERIFY(directory.isValid()); + ApplicationSettings settings(directory.filePath(QStringLiteral("settings.ini")), QSettings::IniFormat); + AppearanceManager manager(&settings, [] { return Qt::ColorScheme::Light; }); + manager.setRequestedMode(AppearanceManager::Mode::Dark); + + ScintillaEdit editor; + editor.setUndoCollection(true); + editor.setText("alpha\nbeta\n"); + editor.setSavePoint(); + editor.gotoPos(editor.length()); + editor.addText(1, "!"); + editor.setSelection(5, 1); + + const QByteArray text = editor.textRange(0, editor.length()); + const sptr_t currentPosition = editor.currentPos(); + const sptr_t anchor = editor.anchor(); + const bool modified = editor.modify(); + const bool canUndo = editor.canUndo(); + + EditorAppearance::apply(&editor, manager.tokens(), QStringLiteral("Courier New"), 11); + + QCOMPARE(editor.styleFore(STYLE_DEFAULT), + static_cast(AppearanceManager::scintillaColor(manager.tokens().textEditor))); + QCOMPARE(editor.styleBack(STYLE_DEFAULT), + static_cast(AppearanceManager::scintillaColor(manager.tokens().surfaceEditor))); + QCOMPARE(static_cast(editor.elementColour(SC_ELEMENT_SELECTION_BACK)), + AppearanceManager::scintillaElementColor(manager.tokens().selectionActive)); + QCOMPARE(editor.textRange(0, editor.length()), text); + QCOMPARE(editor.currentPos(), currentPosition); + QCOMPARE(editor.anchor(), anchor); + QCOMPARE(editor.modify(), modified); + QCOMPARE(editor.canUndo(), canUndo); +} + +void EditorAppearanceTests::restoresNamedStylesAfterStyleClearAll() +{ + QTemporaryDir directory; + ApplicationSettings settings(directory.filePath(QStringLiteral("settings.ini")), QSettings::IniFormat); + AppearanceManager manager(&settings, [] { return Qt::ColorScheme::Dark; }); + manager.setRequestedMode(AppearanceManager::Mode::Dark); + + ScintillaEdit editor; + EditorAppearance::apply(&editor, manager.tokens(), QStringLiteral("Courier New"), 11); + editor.styleClearAll(); + QVERIFY(editor.styleBack(STYLE_LINENUMBER) + != AppearanceManager::scintillaColor(manager.tokens().surfaceShell)); + + EditorAppearance::applyNamedStyles(&editor, manager.tokens()); + + QCOMPARE(editor.styleFore(STYLE_LINENUMBER), + static_cast(AppearanceManager::scintillaColor(manager.tokens().textSecondary))); + QCOMPARE(editor.styleBack(STYLE_LINENUMBER), + static_cast(AppearanceManager::scintillaColor(manager.tokens().surfaceShell))); + QCOMPARE(editor.styleBack(STYLE_INDENTGUIDE), + static_cast(AppearanceManager::scintillaColor(manager.tokens().surfaceEditor))); +} + +void EditorAppearanceTests::switchesTheSameEditorBackToLight() +{ + QTemporaryDir directory; + ApplicationSettings settings(directory.filePath(QStringLiteral("settings.ini")), QSettings::IniFormat); + AppearanceManager manager(&settings, [] { return Qt::ColorScheme::Dark; }); + ScintillaEdit editor; + + manager.setRequestedMode(AppearanceManager::Mode::Dark); + EditorAppearance::apply(&editor, manager.tokens(), QStringLiteral("Courier New"), 11); + QCOMPARE(editor.styleBack(STYLE_DEFAULT), + static_cast(AppearanceManager::scintillaColor(QColor(QStringLiteral("#121314"))))); + + manager.setRequestedMode(AppearanceManager::Mode::Light); + EditorAppearance::apply(&editor, manager.tokens(), QStringLiteral("Courier New"), 11); + QCOMPARE(editor.styleBack(STYLE_DEFAULT), + static_cast(AppearanceManager::scintillaColor(QColor(QStringLiteral("#FFFFFF"))))); +} + +void EditorAppearanceTests::preservesExistingLexerStyleAssignments() +{ + QTemporaryDir directory; + ApplicationSettings settings(directory.filePath(QStringLiteral("settings.ini")), QSettings::IniFormat); + AppearanceManager manager(&settings, [] { return Qt::ColorScheme::Dark; }); + manager.setRequestedMode(AppearanceManager::Mode::Dark); + + ScintillaEdit editor; + editor.setText("token"); + editor.startStyling(0, 0); + editor.setStyling(editor.length(), 7); + QCOMPARE(editor.styleAt(0), 7); + + EditorAppearance::apply(&editor, manager.tokens(), QStringLiteral("Courier New"), 11); + + QCOMPARE(editor.styleAt(0), 7); + editor.styleSetFore(7, AppearanceManager::scintillaColor(manager.tokens().syntaxString)); + QCOMPARE(editor.styleFore(7), + static_cast(AppearanceManager::scintillaColor(manager.tokens().syntaxString))); +} + +QTEST_MAIN(EditorAppearanceTests) + +#include "EditorAppearanceTests.moc" \ No newline at end of file