diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/htmlbuilder.cpp | 103 | ||||
| -rw-r--r-- | src/htmlbuilder.h | 40 | ||||
| -rw-r--r-- | src/messageview.cpp | 16 | ||||
| -rw-r--r-- | src/messageview.h | 8 |
4 files changed, 153 insertions, 14 deletions
diff --git a/src/htmlbuilder.cpp b/src/htmlbuilder.cpp index ccb9171..c8f039f 100644 --- a/src/htmlbuilder.cpp +++ b/src/htmlbuilder.cpp @@ -18,25 +18,92 @@ #include "htmlbuilder.h" +#include <QCoreApplication> +#include <QGuiApplication> #include <QRegularExpression> namespace { -const char *kStyle = R"CSS( -body { font-family: sans-serif; font-size: 10pt; margin: 12px; } +/// The stylesheet, with every colour supplied by the caller. +/// +/// %1 background, %2 text, %3 quote, %4 border, %5 dim. +const char *kStyleTemplate = R"CSS( +body { font-family: sans-serif; font-size: 10pt; margin: 12px; + background: %1; color: %2; } pre.plain { white-space: pre-wrap; word-wrap: break-word; font-family: monospace; margin: 0; } -span.quote { color: #4a6f8a; } -.message { border-top: 1px solid #bbb; padding: 10px 0; } +span.quote { color: %3; } +.message { border-top: 1px solid %4; padding: 10px 0; } .message:first-child { border-top: none; } -.msg-header { font-size: 9pt; color: #555; margin-bottom: 8px; } -.msg-header .who { font-weight: bold; color: #000; } -.stub { font-size: 9pt; color: #666; padding: 4px 0; - border-top: 1px solid #ddd; } +.msg-header { font-size: 9pt; color: %5; margin-bottom: 8px; } +.msg-header .who { font-weight: bold; color: %2; } +.stub { font-size: 9pt; color: %5; padding: 4px 0; + border-top: 1px solid %4; } )CSS"; +/// Mixes two colours, `weight` being how much of `a` survives. +/// +/// Blending is what makes the derived colours theme-correct. A fixed grey is +/// only "subtle" against the background it was chosen for: #555 reads as a +/// quiet label on white and nearly vanishes on near-black. +QColor blend(const QColor &a, const QColor &b, qreal weight) +{ + const qreal inverse = 1.0 - weight; + return QColor::fromRgbF(a.redF() * weight + b.redF() * inverse, + a.greenF() * weight + b.greenF() * inverse, + a.blueF() * weight + b.blueF() * inverse); +} + } // namespace +HtmlBuilder::Palette HtmlBuilder::paletteFrom(const QPalette &palette) +{ + // Base and Text, not Window and WindowText: the pane is a content surface + // like a text edit, and on many themes Base differs from Window. + const QColor background = palette.color(QPalette::Base); + const QColor text = palette.color(QPalette::Text); + + Palette result; + result.background = background; + result.text = text; + + // Both derived from the pair, so they land at the right contrast whichever + // way round the theme is. + result.dim = blend(text, background, 0.6); + result.border = blend(text, background, 0.25); + + // The quote colour keeps its hue, since "this is quoted" is carried by the + // colour being different rather than by it being dimmer, but it is pulled + // toward the background so it stays readable on a dark theme instead of + // glowing. + const QColor quoteHue(0x4a, 0x6f, 0x8a); + result.quote = background.lightnessF() < 0.5 + ? blend(quoteHue.lighter(160), background, 0.75) + : blend(quoteHue, background, 0.85); + + return result; +} + +HtmlBuilder::Palette HtmlBuilder::defaultPalette() +{ + if (const QGuiApplication *app = + qobject_cast<QGuiApplication *>(QCoreApplication::instance())) + return paletteFrom(app->palette()); + + // No GUI application: only reachable from a test that did not pass a + // palette. A default-constructed QPalette is light, which matches what + // this code did before it was themed at all. + return paletteFrom(QPalette()); +} + +QString HtmlBuilder::styleSheet(const Palette &palette) +{ + return QString::fromUtf8(kStyleTemplate) + .arg(palette.background.name(), palette.text.name(), + palette.quote.name(), palette.border.name(), + palette.dim.name()); +} + QString HtmlBuilder::renderPlain(const QString &text) { QString out; @@ -61,12 +128,12 @@ QString HtmlBuilder::renderPlain(const QString &text) return out; } -QString HtmlBuilder::document(const QString &bodyHtml) +QString HtmlBuilder::document(const QString &bodyHtml, const Palette &palette) { return QStringLiteral( "<!DOCTYPE html><html><head><meta charset=\"utf-8\">" "<style>%1</style></head><body>%2</body></html>") - .arg(QString::fromUtf8(kStyle), bodyHtml); + .arg(styleSheet(palette), bodyHtml); } QString HtmlBuilder::namespaceCids(const QString &html, const QString &prefix) @@ -218,14 +285,26 @@ QString HtmlBuilder::renderStub(const ParsedMessage &message) QString HtmlBuilder::build(const ParsedMessage &message, Mode mode) { + return build(message, mode, defaultPalette()); +} + +QString HtmlBuilder::build(const ParsedMessage &message, Mode mode, + const Palette &palette) +{ ThreadRenderItem item; item.message = message; item.expanded = true; - return document(renderBody(item, mode)); + return document(renderBody(item, mode), palette); } QString HtmlBuilder::buildThread(const QList<ThreadRenderItem> &items, Mode mode) { + return buildThread(items, mode, defaultPalette()); +} + +QString HtmlBuilder::buildThread(const QList<ThreadRenderItem> &items, Mode mode, + const Palette &palette) +{ QString body; for (int i = 0; i < items.size(); ++i) { @@ -246,5 +325,5 @@ QString HtmlBuilder::buildThread(const QList<ThreadRenderItem> &items, Mode mode renderBody(item, mode)); } - return document(body); + return document(body, palette); } diff --git a/src/htmlbuilder.h b/src/htmlbuilder.h index 6e306dd..75fd1f8 100644 --- a/src/htmlbuilder.h +++ b/src/htmlbuilder.h @@ -18,7 +18,9 @@ #pragma once +#include <QColor> #include <QList> +#include <QPalette> #include "mimeparser.h" @@ -60,11 +62,46 @@ public: ForcePlain, ///< Always render the plain part, escaped. }; + /// The colours the document's own stylesheet uses. + /// + /// Passed in rather than read from qApp inside the builder, so the CSS can + /// be tested against a known palette without a running application, and so + /// nothing here depends on widget state. + /// + /// **Scope.** These style the chrome around messages and the plain-text + /// render. A message that brings its own HTML brings its own colours, and + /// those are deliberately left alone: rewriting a sender's styling would + /// break layouts that depend on it, and a newsletter that sets a white + /// background is entitled to stay white. + struct Palette { + QColor background; ///< The pane itself. + QColor text; ///< Body text. + QColor dim; ///< Headers and stubs: present but secondary. + QColor border; ///< Rules between messages. + QColor quote; ///< Quoted lines in plain text. + }; + + /// Derives the document palette from a widget palette. + /// + /// The dim and border colours are blends rather than fixed greys, which is + /// what makes this work on a dark theme: a hardcoded #555 that reads as + /// "subtle" on white is nearly invisible on near-black. + static Palette paletteFrom(const QPalette &palette); + + /// The palette used when a caller supplies none: the running application's. + /// Falls back to a light default with no QApplication, which only happens + /// in a test that did not ask for a palette. + static Palette defaultPalette(); + /// Single message, used for the error card and for tests. static QString build(const ParsedMessage &message, Mode mode); + static QString build(const ParsedMessage &message, Mode mode, + const Palette &palette); /// The whole thread, oldest first. static QString buildThread(const QList<ThreadRenderItem> &items, Mode mode); + static QString buildThread(const QList<ThreadRenderItem> &items, Mode mode, + const Palette &palette); /// Rewrites cid: URLs in an HTML body to their namespaced form. static QString namespaceCids(const QString &html, const QString &prefix); @@ -73,5 +110,6 @@ private: static QString renderPlain(const QString &text); static QString renderBody(const ThreadRenderItem &item, Mode mode); static QString renderStub(const ParsedMessage &message); - static QString document(const QString &bodyHtml); + static QString document(const QString &bodyHtml, const Palette &palette); + static QString styleSheet(const Palette &palette); }; diff --git a/src/messageview.cpp b/src/messageview.cpp index d7167bf..10b21a9 100644 --- a/src/messageview.cpp +++ b/src/messageview.cpp @@ -378,12 +378,26 @@ void MessageView::showDetailsDialog() dialog.exec(); } +void MessageView::changeEvent(QEvent *event) +{ + QWidget::changeEvent(event); + + // Only when there is something to re-render: rendering an empty item list + // would replace a deliberately blank pane with an empty document. + if (event->type() == QEvent::PaletteChange && !m_items.isEmpty()) + render(); +} + void MessageView::render() { const HtmlBuilder::Mode mode = m_preferHtml ? HtmlBuilder::PreferHtml : HtmlBuilder::ForcePlain; - setDocument(HtmlBuilder::buildThread(m_items, mode)); + // This widget's palette, not the application's: a style sheet or a themed + // parent can give the pane different colours from qApp, and the document + // has to match the frame it sits in rather than the app default. + setDocument(HtmlBuilder::buildThread(m_items, mode, + HtmlBuilder::paletteFrom(palette()))); rebuildAttachmentBar(); // Blocking is discovered during load, so check shortly afterwards. diff --git a/src/messageview.h b/src/messageview.h index 1be17f8..c55f5c5 100644 --- a/src/messageview.h +++ b/src/messageview.h @@ -105,6 +105,14 @@ protected: /// rather than one widget. bool eventFilter(QObject *watched, QEvent *event) override; + /// Re-renders when the desktop theme changes. + /// + /// The document's colours are baked into its stylesheet at build time, so + /// unlike a widget it does not restyle itself: switching the desktop from + /// light to dark would otherwise leave the open thread on the old palette + /// until the next selection. + void changeEvent(QEvent *event) override; + private: void render(); void updateHeader(); |
