kristall/src/documentstyle.cpp

579 lines
21 KiB
C++

#include "documentstyle.hpp"
#include "kristall.hpp"
#include <cassert>
#include <QDebug>
#include <QString>
#include <QStringList>
#include <QFontDatabase>
#include <QFontInfo>
#include <QCryptographicHash>
#include <QDebug>
#include <cctype>
#include <array>
#include <cmath>
DocumentStyle::DefaultFonts::DefaultFonts()
{
// Initialise default fonts
#ifdef Q_OS_WIN32
// Windows default fonts are ugly, so we use standard ones.
this->regular = "Segoe UI";
this->fixed = "Consolas";
#else
// *nix
regular = QFontDatabase::systemFont(QFontDatabase::GeneralFont).family();
fixed = QFontInfo(QFont("monospace")).family();
#endif
}
static QString encodeCssFont (const QFont& refFont)
{
//-----------------------------------------------------------------------
// This function assembles a CSS Font specification string from
// a QFont. This supports most of the QFont attributes settable in
// the Qt 4.8 and Qt 5.3 QFontDialog.
//
// (1) Font Family
// (2) Font Weight (just bold or not)
// (3) Font Style (possibly Italic or Oblique)
// (4) Font Size (in either pixels or points)
// (5) Decorations (possibly Underline or Strikeout)
//
// Not supported: Writing System (e.g. Latin).
//
// See the corresponding decode function, below.
// QFont decodeCssFontString (const QString cssFontStr)
//-----------------------------------------------------------------------
QStringList fields; // CSS font attribute fields
// ***************************************************
// *** (1) Font Family: Primary plus Substitutes ***
// ***************************************************
const QString family = refFont.family();
// NOTE [9-2014, Qt 4.8.6]: This isn't what I thought it was. It
// does not return a list of "fallback" font faces (e.g. Georgia,
// Serif for "Times New Roman"). In my testing, this is always
// returning an empty list.
//
QStringList famSubs = QFont::substitutes (family);
if (!famSubs.contains (family))
famSubs.prepend (family);
static const QChar DBL_QUOT ('"');
const int famCnt = famSubs.count();
QStringList famList;
for (int inx = 0; inx < famCnt; ++inx)
{
// Place double quotes around family names having space characters,
// but only if double quotes are not already there.
//
const QString fam = famSubs [inx];
if (fam.contains (' ') && !fam.startsWith (DBL_QUOT))
famList << (DBL_QUOT + fam + DBL_QUOT);
else
famList << fam;
}
const QString famStr = QString ("font-family: ") + famList.join (", ");
fields << famStr;
// **************************************
// *** (2) Font Weight: Bold or Not ***
// **************************************
const bool bold = refFont.bold();
if (bold)
fields << "font-weight: bold";
// ****************************************************
// *** (3) Font Style: possibly Italic or Oblique ***
// ****************************************************
const QFont::Style style = refFont.style();
switch (style)
{
case QFont::StyleNormal: break;
case QFont::StyleItalic: fields << "font-style: italic"; break;
case QFont::StyleOblique: fields << "font-style: oblique"; break;
}
// ************************************************
// *** (4) Font Size: either Pixels or Points ***
// ************************************************
const double sizeInPoints = refFont.pointSizeF(); // <= 0 if not defined.
const int sizeInPixels = refFont.pixelSize(); // <= 0 if not defined.
if (sizeInPoints > 0.0)
fields << QString ("font-size: %1pt") .arg (sizeInPoints);
else if (sizeInPixels > 0)
fields << QString ("font-size: %1px") .arg (sizeInPixels);
// ***********************************************
// *** (5) Decorations: Underline, Strikeout ***
// ***********************************************
const bool underline = refFont.underline();
const bool strikeOut = refFont.strikeOut();
if (underline && strikeOut)
fields << "text-decoration: underline line-through";
else if (underline)
fields << "text-decoration: underline";
else if (strikeOut)
fields << "text-decoration: line-through";
const QString cssFontStr = fields.join ("; ");
return cssFontStr;
}
DocumentStyle::DocumentStyle() : theme(Fixed),
standard_font(),
h1_font(),
h2_font(),
h3_font(),
preformatted_font(),
blockquote_font(),
background_color(0xed, 0xef, 0xff),
standard_color(0x00, 0x00, 0x00),
preformatted_color(0x00, 0x00, 0x00),
h1_color(0x02, 0x2f, 0x90),
h2_color(0x02, 0x2f, 0x90),
h3_color(0x02, 0x2f, 0x90),
blockquote_fgcolor(0x00, 0x00, 0x00),
blockquote_bgcolor(0xFF, 0xFF, 0xFF),
internal_link_color(0x0e, 0x8f, 0xff),
external_link_color(0x0e, 0x8f, 0xff),
cross_scheme_link_color(0x09, 0x60, 0xa7),
internal_link_prefix(""),
external_link_prefix(""),
margin_h(30.0),
margin_v(55.0),
text_width(900),
ansi_colors({"black", "darkred", "darkgreen", "darkgoldenrod",
"darkblue", "darkmagenta", "darkcyan", "lightgray",
"gray", "red", "green", "goldenrod",
"lightblue", "magenta", "cyan", "white"}),
justify_text(true),
text_width_enabled(true),
centre_h1(false),
line_height_p(5.0),
line_height_h(5.0),
indent_bq(1), indent_p(1), indent_h(0), indent_l(2),
indent_size(15.0),
list_symbol(QTextListFormat::ListDisc)
{
this->initialiseDefaultFonts();
}
void DocumentStyle::initialiseDefaultFonts()
{
DefaultFonts default_fonts;
preformatted_font.setFamily(default_fonts.fixed);
preformatted_font.setPointSizeF(12.0);
standard_font.setFamily(default_fonts.regular);
standard_font.setPointSizeF(12.0);
h1_font.setFamily(default_fonts.regular);
h1_font.setBold(true);
h1_font.setPointSizeF(22.0);
h2_font.setFamily(default_fonts.regular);
h2_font.setBold(true);
h2_font.setPointSizeF(17.0);
h3_font.setFamily(default_fonts.regular);
h3_font.setBold(true);
h3_font.setPointSizeF(14.0);
blockquote_font.setFamily(default_fonts.regular);
blockquote_font.setItalic(true);
blockquote_font.setPointSizeF(12.0);
}
QString DocumentStyle::createFileNameFromName(const QString &src, int index)
{
QString result;
result.reserve(src.size() + 5);
for(int i = 0; i < src.size(); i++)
{
QChar c = src.at(i);
if(c.isLetterOrNumber()) {
result.append(c.toLower());
}
else if(c.isSpace()) {
result.append('-');
}
else {
result.append(QString::number(c.unicode()));
}
}
if(index > 0) {
result.append(QString("-%1").arg(index));
}
result.append(".kthm");
return result;
}
bool DocumentStyle::save(QSettings &settings) const
{
settings.setValue("version", 1);
settings.setValue("theme", int(theme));
settings.setValue("background_color", background_color.name());
settings.setValue("blockquote_color", blockquote_bgcolor.name());
settings.setValue("margins_h", margin_h);
settings.setValue("margins_v", margin_v);
settings.setValue("ansi_colors", ansi_colors);
{
settings.beginGroup("Standard");
settings.setValue("font", standard_font.toString());
settings.setValue("color", standard_color.name());
settings.endGroup();
}
{
settings.beginGroup("Preformatted");
settings.setValue("font", preformatted_font.toString());
settings.setValue("color", preformatted_color.name());
settings.endGroup();
}
{
settings.beginGroup("H1");
settings.setValue("font", h1_font.toString());
settings.setValue("color", h1_color.name());
settings.endGroup();
}
{
settings.beginGroup("H2");
settings.setValue("font", h2_font.toString());
settings.setValue("color", h2_color.name());
settings.endGroup();
}
{
settings.beginGroup("H3");
settings.setValue("font", h3_font.toString());
settings.setValue("color", h3_color.name());
settings.endGroup();
}
{
settings.beginGroup("Blockquote");
settings.setValue("font", blockquote_font.toString());
settings.setValue("color", blockquote_fgcolor.name());
settings.endGroup();
}
{
settings.beginGroup("Link");
settings.setValue("color_internal", internal_link_color.name());
settings.setValue("color_external", external_link_color.name());
settings.setValue("color_cross_scheme", cross_scheme_link_color.name());
settings.setValue("internal_prefix", internal_link_prefix);
settings.setValue("external_prefix", external_link_prefix);
settings.endGroup();
}
{
settings.beginGroup("Formatting");
settings.setValue("justify_text", justify_text);
settings.setValue("text_width_enabled", text_width_enabled);
settings.setValue("centre_h1", centre_h1);
settings.setValue("text_width", text_width);
settings.setValue("line_height_p", line_height_p);
settings.setValue("line_height_h", line_height_h);
settings.setValue("indent_bq", indent_bq);
settings.setValue("indent_p", indent_p);
settings.setValue("indent_h", indent_h);
settings.setValue("indent_l", indent_l);
settings.setValue("indent_size", indent_size);
settings.setValue("list_symbol", (int)list_symbol);
settings.endGroup();
}
return true;
}
bool DocumentStyle::load(QSettings &settings)
{
switch(settings.value("version", 0).toInt())
{
case 0: {
if(settings.contains("standard_color"))
{
standard_font.fromString(settings.value("standard_font").toString());
h1_font.fromString(settings.value("h1_font").toString());
h2_font.fromString(settings.value("h2_font").toString());
h3_font.fromString(settings.value("h3_font").toString());
preformatted_font.fromString(settings.value("preformatted_font").toString());
blockquote_font.fromString(settings.value("standard_font").toString());
background_color = QColor(settings.value("background_color").toString());
standard_color = QColor(settings.value("standard_color").toString());
preformatted_color = QColor(settings.value("preformatted_color").toString());
blockquote_bgcolor = QColor(settings.value("blockquote_color").toString());
blockquote_fgcolor = standard_color;
h1_color = QColor(settings.value("h1_color").toString());
h2_color = QColor(settings.value("h2_color").toString());
h3_color = QColor(settings.value("h3_color").toString());
internal_link_color = QColor(settings.value("internal_link_color").toString());
external_link_color = QColor(settings.value("external_link_color").toString());
cross_scheme_link_color = QColor(settings.value("cross_scheme_link_color").toString());
internal_link_prefix = settings.value("internal_link_prefix").toString();
external_link_prefix = settings.value("external_link_prefix").toString();
margin_h = margin_v = settings.value("margins").toDouble();
theme = Theme(settings.value("theme").toInt());
}
break;
}
case 1: {
theme = Theme(settings.value("theme", int(theme)).toInt());
background_color = QColor { settings.value("background_color", background_color.name()).toString() };
blockquote_bgcolor = QColor { settings.value("blockquote_color", blockquote_bgcolor.name()).toString() };
margin_h = settings.value("margins_h", 30).toInt();
margin_v = settings.value("margins_v", 55).toInt();
QStringList default_colors = {"black", "darkred", "darkgreen", "darkgoldenrod",
"darkblue", "darkmagenta", "darkcyan", "lightgray",
"gray", "red", "green", "goldenrod",
"lightblue", "magenta", "cyan", "white"};
ansi_colors = settings.value("ansi_colors", default_colors).toStringList();
{
settings.beginGroup("Standard");
standard_font.fromString(settings.value("font", standard_font.toString()).toString());
standard_color = QString { settings.value("color", standard_color.name()).toString() };
settings.endGroup();
}
{
settings.beginGroup("Preformatted");
preformatted_font.fromString(settings.value("font", preformatted_font.toString()).toString());
preformatted_color = QString { settings.value("color", preformatted_color.name()).toString() };
settings.endGroup();
}
{
settings.beginGroup("H1");
h1_font.fromString(settings.value("font", h1_font.toString()).toString());
h1_color = QString { settings.value("color", h1_color.name()).toString() };
settings.endGroup();
}
{
settings.beginGroup("H2");
h2_font.fromString(settings.value("font", h2_font.toString()).toString());
h2_color = QString { settings.value("color", h2_color.name()).toString() };
settings.endGroup();
}
{
settings.beginGroup("H3");
h3_font.fromString(settings.value("font", h3_font.toString()).toString());
h3_color = QString { settings.value("color", h3_color.name()).toString() };
settings.endGroup();
}
{
settings.beginGroup("Blockquote");
blockquote_font.fromString(settings.value("font", standard_font.toString()).toString());
blockquote_fgcolor = QString { settings.value("color", standard_color.name()).toString() };
settings.endGroup();
}
{
settings.beginGroup("Link");
internal_link_color = QString { settings.value("color_internal", internal_link_color.name()).toString() };
external_link_color = QString {settings.value("color_external", external_link_color.name()).toString() };
cross_scheme_link_color = QString {settings.value("color_cross_scheme", cross_scheme_link_color.name()).toString() };
internal_link_prefix = settings.value("internal_prefix", internal_link_prefix).toString();
external_link_prefix = settings.value("external_prefix", external_link_prefix).toString();
settings.endGroup();
}
{
settings.beginGroup("Formatting");
justify_text = settings.value("justify_text", justify_text).toBool();
text_width_enabled = settings.value("text_width_enabled", text_width_enabled).toBool();
centre_h1 = settings.value("centre_h1", centre_h1).toBool();
text_width = settings.value("text_width", text_width).toInt();
line_height_p = settings.value("line_height_p", line_height_p).toDouble();
line_height_h = settings.value("line_height_h", line_height_h).toDouble();
indent_bq = settings.value("indent_bq", indent_bq).toInt();
indent_p = settings.value("indent_p", indent_p).toInt();
indent_h = settings.value("indent_h", indent_h).toInt();
indent_l = settings.value("indent_l", indent_l).toInt();
indent_size = settings.value("indent_size", indent_size).toDouble();
list_symbol = (QTextListFormat::Style)settings.value("list_symbol", list_symbol).toInt();
settings.endGroup();
}
} break;
default:
return false;
}
return true;
}
DocumentStyle DocumentStyle::derive(const QUrl &url) const
{
DocumentStyle themed = *this;
// Patch font lists to allow improved emoji display:
static QStringList emojiFonts = {
"<PLACEHOLDER>",
"<FALLBACK>",
"Apple Color Emoji",
"Segoe UI Emoji",
"Twitter Color Emoji",
"Noto Color Emoji",
"JoyPixels",
};
DefaultFonts default_fonts;
auto const patchup_font = [&default_fonts](QFont & font, bool fixed=false)
{
// Set the "fallback" font, just to be absolutely sure.
// Note the main purpose of this is to avoid emoji fonts
// from taking precedence over text fonts.
// (fixes *nix default font issues)
emojiFonts[1] = fixed
? default_fonts.fixed
: default_fonts.regular;
// Set the primary font as the preferred font.
// We ensure that the font family is available first,
// so that we don't get an ugly default font
// (fixes Windows' default font problem)
QFontDatabase db;
if (!db.families().contains(font.family()))
{
emojiFonts.front() = fixed
? default_fonts.fixed
: default_fonts.regular;
}
else
{
emojiFonts.front() = font.family();
}
// Set emoji fonts if supported and enabled.
if (kristall::EMOJIS_SUPPORTED &&
kristall::globals().options.emojis_enabled)
{
// Redundant check to make compiler happy...
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
font.setFamilies(emojiFonts);
#endif
}
else
{
font.setFamily(emojiFonts.front());
}
};
patchup_font(themed.h1_font);
patchup_font(themed.h2_font);
patchup_font(themed.h3_font);
patchup_font(themed.standard_font);
patchup_font(themed.preformatted_font, true);
patchup_font(themed.blockquote_font);
if (this->theme == Fixed)
return themed;
QByteArray hash = QCryptographicHash::hash(url.host().toUtf8(), QCryptographicHash::Md5);
std::array<uint8_t, 16> items;
assert(items.size() == hash.size());
memcpy(items.data(), hash.data(), items.size());
float hue = (items[0] + items[1]) / 510.0;
float saturation = items[2] / 255.0;
double tmp;
switch (this->theme)
{
case AutoDarkTheme:
{
themed.background_color = QColor::fromHslF(hue, saturation, 0.25f);
themed.standard_color = QColor{0xFF, 0xFF, 0xFF};
themed.h1_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.75);
themed.h2_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.75);
themed.h3_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.75);
themed.external_link_color = QColor::fromHslF(std::modf(hue + 0.25, &tmp), 1.0, 0.75);
themed.internal_link_color = themed.external_link_color.lighter(110);
themed.cross_scheme_link_color = themed.external_link_color.darker(110);
themed.blockquote_bgcolor = themed.background_color.lighter(130);
themed.blockquote_fgcolor = QColor{0xEE, 0xEE, 0xEE};
break;
}
case AutoLightTheme:
{
themed.background_color = QColor::fromHslF(hue, items[2] / 255.0, 0.85);
themed.standard_color = QColor{0x00, 0x00, 0x00};
themed.h1_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.25);
themed.h2_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.25);
themed.h3_color = QColor::fromHslF(std::modf(hue + 0.5, &tmp), 1.0 - saturation, 0.25);
themed.external_link_color = QColor::fromHslF(std::modf(hue + 0.25, &tmp), 1.0, 0.25);
themed.internal_link_color = themed.external_link_color.darker(110);
themed.cross_scheme_link_color = themed.external_link_color.lighter(110);
themed.blockquote_bgcolor = themed.background_color.darker(113);
themed.blockquote_fgcolor = QColor{0x40, 0x40, 0x40};
break;
}
case Fixed:
assert(false);
}
// Same for all themes
themed.preformatted_color = themed.standard_color;
return themed;
}
QString DocumentStyle::toStyleSheet() const
{
QString css;
css += QString("p { color: %2; %1 }\n").arg(encodeCssFont (standard_font), standard_color.name());
css += QString("a { color: %2; %1 }\n").arg(encodeCssFont (standard_font), external_link_color.name());
css += QString("pre { color: %2; %1 }\n").arg(encodeCssFont (preformatted_font), preformatted_color.name());
css += QString("h1 { color: %2; %1 }\n").arg(encodeCssFont (h1_font), h1_color.name());
css += QString("h2 { color: %2; %1 }\n").arg(encodeCssFont (h2_font), h2_color.name());
css += QString("h3 { color: %2; %1 }\n").arg(encodeCssFont (h3_font), h3_color.name());
css += QString("blockquote { background: %1; color: %2; %3 }\n")
.arg(blockquote_bgcolor.name(), blockquote_fgcolor.name(), encodeCssFont(blockquote_font));
// qDebug() << "CSS → " << css;
return css;
}