aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/htmlbuilder.cpp103
-rw-r--r--src/htmlbuilder.h40
-rw-r--r--src/messageview.cpp16
-rw-r--r--src/messageview.h8
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();