Files

530 lines
13 KiB
C++

#include <KSvg/FrameSvg>
#include <QFile>
#include <QFileInfo>
#include <QGuiApplication>
#include <QHash>
#include <QIODevice>
#include <QSet>
#include <QSizeF>
#include <QString>
#include <QXmlStreamReader>
#include <QtTypes>
#include <cmath>
#include <cstddef>
#include <initializer_list>
#include <iostream>
namespace {
struct ElementInfo {
QString name;
QHash<QString, QString> attributes;
};
struct Orientation {
const char* name;
qreal originX;
qreal left;
qreal top;
qreal right;
qreal bottom;
};
struct State {
const char* name;
qreal originY;
};
struct Geometry {
qreal x;
qreal y;
qreal width;
qreal height;
};
constexpr qreal comparisonTolerance = 0.001;
constexpr qreal markerOffset = 20;
constexpr qreal markerSize = 4;
constexpr qreal testFrameHeight = 32;
constexpr qreal testFrameWidth = 36;
constexpr qreal tileSize = 44;
constexpr std::initializer_list<Orientation> orientations = {
{"", 0, 6, 8, 6, 12},
{"north", 44, 6, 12, 6, 8 },
{"west", 88, 12, 6, 8, 6 },
{"east", 132, 8, 6, 12, 6 },
};
constexpr std::initializer_list<State> frameStates = {
{"normal", 0 },
{"minimized", 44 },
{"focus", 88 },
{"hover", 132},
{"progress", 0 },
{"attention", 0 },
};
constexpr std::initializer_list<State> marginStates = {
{"normal", 0 },
{"minimized", 44 },
{"focus", 88 },
{"hover", 132},
};
constexpr std::initializer_list<const char*> frameParts = {
"center",
"top",
"bottom",
"left",
"right",
"topleft",
"topright",
"bottomleft",
"bottomright",
};
constexpr std::initializer_list<const char*> marginSides = {
"top",
"bottom",
"left",
"right",
};
QString framePrefix(const Orientation& orientation, const State& state) {
if (QString::fromLatin1(orientation.name).isEmpty()) {
return QString::fromLatin1(state.name);
}
return QStringLiteral("%1-%2").arg(
QString::fromLatin1(orientation.name), QString::fromLatin1(state.name)
);
}
QString marginHintId(
const Orientation& orientation, const State& state, const char* side
) {
return QStringLiteral("%1-hint-%2-margin")
.arg(framePrefix(orientation, state), QString::fromLatin1(side));
}
bool closeEnough(qreal actual, qreal expected) {
return std::abs(actual - expected) < comparisonTolerance;
}
bool checkValue(
const QString& context, const char* name, qreal actual, qreal expected
) {
if (closeEnough(actual, expected)) {
return true;
}
std::cerr << context.toStdString() << ": expected " << name << "=" << expected
<< ", got " << actual << "\n";
return false;
}
bool readElements(const QString& path, QHash<QString, ElementInfo>& elements) {
QFile file(path);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
std::cerr << path.toStdString() << ": cannot open file\n";
return false;
}
QXmlStreamReader reader(&file);
bool success = true;
while (!reader.atEnd()) {
reader.readNext();
if (!reader.isStartElement()) {
continue;
}
const QString elementId =
reader.attributes().value(QStringLiteral("id")).toString();
if (elementId.isEmpty()) {
continue;
}
if (elements.contains(elementId)) {
std::cerr << path.toStdString() << ": " << elementId.toStdString()
<< ": duplicate element id\n";
success = false;
continue;
}
ElementInfo element;
element.name = reader.name().toString();
for (const QXmlStreamAttribute& attribute : reader.attributes()) {
element.attributes.insert(
attribute.name().toString(), attribute.value().toString()
);
}
elements.insert(elementId, element);
}
if (reader.hasError()) {
std::cerr << path.toStdString()
<< ": invalid XML: " << reader.errorString().toStdString()
<< "\n";
success = false;
}
return success;
}
bool parseNumber(
const QString& path,
const QString& elementId,
const ElementInfo& element,
const char* attributeName,
qreal& value
) {
const QString attribute = QString::fromLatin1(attributeName);
if (!element.attributes.contains(attribute)) {
std::cerr << path.toStdString() << ": " << elementId.toStdString()
<< ": missing " << attributeName << "\n";
return false;
}
bool parsed = false;
value = element.attributes.value(attribute).toDouble(&parsed);
if (!parsed) {
std::cerr << path.toStdString() << ": " << elementId.toStdString()
<< ": invalid " << attributeName << " value "
<< element.attributes.value(attribute).toStdString() << "\n";
return false;
}
return true;
}
Geometry expectedMarginGeometry(
const Orientation& orientation, const State& state, const QString& side
) {
if (side == QStringLiteral("top")) {
return {
orientation.originX + markerOffset,
state.originY,
markerSize,
orientation.top
};
}
if (side == QStringLiteral("bottom")) {
return {
orientation.originX + markerOffset,
state.originY + tileSize - orientation.bottom,
markerSize,
orientation.bottom,
};
}
if (side == QStringLiteral("left")) {
return {
orientation.originX,
state.originY + markerOffset,
orientation.left,
markerSize
};
}
return {
orientation.originX + tileSize - orientation.right,
state.originY + markerOffset,
orientation.right,
markerSize,
};
}
bool checkFrameContract(
const QString& path, const QHash<QString, ElementInfo>& elements
) {
bool success = true;
for (const Orientation& orientation : orientations) {
for (const State& state : frameStates) {
const QString prefix = framePrefix(orientation, state);
if (!elements.contains(prefix)) {
std::cerr << path.toStdString() << ": " << prefix.toStdString()
<< ": missing frame group\n";
success = false;
}
for (const char* part : frameParts) {
const QString elementId = QStringLiteral("%1-%2").arg(
prefix, QString::fromLatin1(part)
);
if (!elements.contains(elementId)) {
std::cerr << path.toStdString() << ": " << elementId.toStdString()
<< ": missing frame part\n";
success = false;
}
}
}
}
return success;
}
bool checkMarginHint(
const QString& path,
const QHash<QString, ElementInfo>& elements,
const QString& elementId,
const Geometry& expected
) {
const ElementInfo element = elements.value(elementId);
if (element.name != QStringLiteral("rect")) {
std::cerr << path.toStdString() << ": " << elementId.toStdString()
<< ": expected rect element\n";
return false;
}
bool success = true;
qreal actual = 0;
if (parseNumber(path, elementId, element, "x", actual)) {
success = checkValue(
QStringLiteral("%1: %2").arg(path, elementId),
"x",
actual,
expected.x
) &&
success;
} else {
success = false;
}
if (parseNumber(path, elementId, element, "y", actual)) {
success = checkValue(
QStringLiteral("%1: %2").arg(path, elementId),
"y",
actual,
expected.y
) &&
success;
} else {
success = false;
}
if (parseNumber(path, elementId, element, "width", actual)) {
success = checkValue(
QStringLiteral("%1: %2").arg(path, elementId),
"width",
actual,
expected.width
) &&
success;
} else {
success = false;
}
if (parseNumber(path, elementId, element, "height", actual)) {
success = checkValue(
QStringLiteral("%1: %2").arg(path, elementId),
"height",
actual,
expected.height
) &&
success;
} else {
success = false;
}
return success;
}
bool checkMarginContract(
const QString& path, const QHash<QString, ElementInfo>& elements
) {
bool success = true;
QSet<QString> expectedIds;
for (const Orientation& orientation : orientations) {
for (const State& state : marginStates) {
for (const char* sideName : marginSides) {
const QString side = QString::fromLatin1(sideName);
const QString elementId = marginHintId(orientation, state, sideName);
expectedIds.insert(elementId);
if (!elements.contains(elementId)) {
std::cerr << path.toStdString() << ": " << elementId.toStdString()
<< ": missing margin hint\n";
success = false;
continue;
}
const Geometry expected = expectedMarginGeometry(
orientation, state, side
);
success = checkMarginHint(path, elements, elementId, expected) &&
success;
}
}
}
for (auto iterator = elements.constBegin(); iterator != elements.constEnd();
++iterator) {
const QString& elementId = iterator.key();
bool isMarginHint = false;
for (const char* side : marginSides) {
isMarginHint = isMarginHint ||
elementId.endsWith(QStringLiteral("-hint-%1-margin")
.arg(QString::fromLatin1(side)));
}
if (isMarginHint && !expectedIds.contains(elementId)) {
std::cerr << path.toStdString() << ": " << elementId.toStdString()
<< ": unexpected margin hint\n";
success = false;
}
}
return success;
}
bool checkStretchHint(
const QString& path, const QHash<QString, ElementInfo>& elements
) {
const QString elementId = QStringLiteral("hint-stretch-borders");
if (!elements.contains(elementId)) {
std::cerr << path.toStdString()
<< ": hint-stretch-borders: missing stretch hint\n";
return false;
}
const QString name = elements.value(elementId).name;
if (name == QStringLiteral("path") || name == QStringLiteral("rect")) {
return true;
}
std::cerr << path.toStdString()
<< ": hint-stretch-borders: expected path or rect element\n";
return false;
}
bool checkSvgContract(const QString& path) {
QHash<QString, ElementInfo> elements;
if (!readElements(path, elements)) {
return false;
}
bool success = true;
success = checkFrameContract(path, elements) && success;
success = checkMarginContract(path, elements) && success;
success = checkStretchHint(path, elements) && success;
return success;
}
bool checkKsvgPrefixes(const QString& path) {
KSvg::FrameSvg frame;
frame.setUsingRenderingCache(false);
frame.setImagePath(path);
bool success = true;
for (const Orientation& orientation : orientations) {
for (const State& state : frameStates) {
const QString prefix = framePrefix(orientation, state);
if (!frame.hasElementPrefix(prefix)) {
std::cerr << prefix.toStdString() << ": missing KSVG frame prefix\n";
success = false;
}
}
}
return success;
}
bool checkKsvgMargins(const QString& path) {
bool success = true;
for (const Orientation& orientation : orientations) {
for (const State& state : marginStates) {
const QString prefix = framePrefix(orientation, state);
KSvg::FrameSvg frame;
frame.setUsingRenderingCache(false);
frame.setImagePath(path);
frame.setElementPrefix(prefix);
frame.resizeFrame(QSizeF(testFrameWidth, testFrameHeight));
qreal left = 0;
qreal top = 0;
qreal right = 0;
qreal bottom = 0;
frame.getFixedMargins(left, top, right, bottom);
success = checkValue(prefix, "left", left, orientation.left) && success;
success = checkValue(prefix, "top", top, orientation.top) && success;
success = checkValue(prefix, "right", right, orientation.right) &&
success;
success = checkValue(prefix, "bottom", bottom, orientation.bottom) &&
success;
success = checkValue(
prefix,
"minimumWidth",
frame.minimumDrawingWidth(),
orientation.left + orientation.right
) &&
success;
success = checkValue(
prefix,
"minimumHeight",
frame.minimumDrawingHeight(),
orientation.top + orientation.bottom
) &&
success;
}
}
return success;
}
} // namespace
int main(int argc, char** argv) {
const QGuiApplication app(argc, argv);
if (argc != 3) {
std::cerr << "usage: " << argv[0]
<< " widgets/tasks.svg widgets/tasks.svgz\n";
return 2;
}
const QString svgPath =
QFileInfo(QString::fromLocal8Bit(argv[1])).absoluteFilePath();
const QString svgzPath =
QFileInfo(QString::fromLocal8Bit(argv[2])).absoluteFilePath();
bool success = true;
success = checkSvgContract(svgPath) && success;
success = checkKsvgPrefixes(svgzPath) && success;
success = checkKsvgMargins(svgzPath) && success;
if (!success) {
return 1;
}
const size_t frameCount = orientations.size() * frameStates.size();
const size_t hintCount = orientations.size() * marginStates.size() *
marginSides.size();
std::cout << "ok: " << frameCount << " task frames, " << hintCount
<< " margin hints, and KSVG task margins\n";
return 0;
}