207 lines
5.2 KiB
C++
207 lines
5.2 KiB
C++
#include "alignaudit.hpp"
|
|
|
|
#include <QDateTime>
|
|
#include <QFile>
|
|
#include <QGuiApplication>
|
|
#include <QQmlContext>
|
|
#include <QRegularExpression>
|
|
#include <QScreen>
|
|
#include <QTextStream>
|
|
#include <QTimer>
|
|
#include <QQuickItem>
|
|
#include <QQuickWindow>
|
|
#include <cmath>
|
|
#include <functional>
|
|
#include <qobject.h>
|
|
|
|
#ifdef ALIGN_AUDIT_USE_QML_PRIVATE
|
|
#include <private/qqmldata_p.h>
|
|
#endif
|
|
|
|
namespace ZShell {
|
|
namespace {
|
|
|
|
bool isOff(double v, double tol) {
|
|
return std::abs(v - std::round(v)) > tol;
|
|
}
|
|
|
|
QString num(double v, bool off) {
|
|
return QString::number(v, 'f', 2) + (off ? QStringLiteral("*") : QString());
|
|
}
|
|
|
|
QString typeName(const QQuickItem* item) {
|
|
static const QRegularExpression suffix(QStringLiteral("_QML(TYPE)?_\\d+$"));
|
|
QString name = QString::fromUtf8(item->metaObject()->className());
|
|
name.remove(suffix);
|
|
if (name.startsWith(QLatin1String("QQuick"))) {
|
|
name.remove(0, 6);
|
|
}
|
|
return name;
|
|
}
|
|
|
|
QString label(const QQuickItem* item) {
|
|
QString s = typeName(item);
|
|
if (!item->objectName().isEmpty()) {
|
|
s += QLatin1Char('#') + item->objectName();
|
|
}
|
|
return s;
|
|
}
|
|
|
|
QString ancestry(const QQuickItem* item, int maxDepth = 8) {
|
|
QStringList parts;
|
|
for (const QQuickItem* i = item; i && parts.size() < maxDepth;
|
|
i = i->parentItem()) {
|
|
parts.prepend(label(i));
|
|
}
|
|
return parts.join(QLatin1String(" > "));
|
|
}
|
|
|
|
struct Source {
|
|
QString file;
|
|
int line = 0;
|
|
int column = 0;
|
|
};
|
|
|
|
Source sourceOf(QQuickItem* item) {
|
|
Source s;
|
|
if (const QQmlContext* ctx = qmlContext(item)) {
|
|
const QUrl url = ctx->baseUrl();
|
|
s.file = url.isLocalFile() ? url.toLocalFile() : url.toString();
|
|
}
|
|
#ifdef ALIGN_AUDIT_USE_QML_PRIVATE
|
|
if (const QQmlData* d = QQmlData::get(item, false)) {
|
|
s.line = int(d->lineNumber);
|
|
s.column = int(d->columnNumber);
|
|
}
|
|
#endif
|
|
return s;
|
|
}
|
|
|
|
void visit(QQuickItem* item, const std::function<void(QQuickItem*)>& fn) {
|
|
if (!item->isVisible() || item->opacity() <= 0.0) {
|
|
return; // whole subtree is invisible
|
|
}
|
|
fn(item);
|
|
for (QQuickItem* child : item->childItems()) {
|
|
visit(child, fn);
|
|
}
|
|
}
|
|
|
|
int auditWindow(QQuickWindow* window, const Options& o, QTextStream& out) {
|
|
const double dpr = window->effectiveDevicePixelRatio();
|
|
const double tol = o.tolerance;
|
|
int count = 0;
|
|
|
|
visit(window->contentItem(), [&](QQuickItem* item) {
|
|
if (item->width() <= 0 || item->height() <= 0) {
|
|
return;
|
|
}
|
|
if (o.paintedOnly &&
|
|
!item->flags().testFlag(QQuickItem::ItemHasContents) &&
|
|
!item->clip()) {
|
|
return;
|
|
}
|
|
|
|
const bool isText = item->inherits("QQuickText");
|
|
|
|
const QRectF r =
|
|
item->mapRectToScene(QRectF(0, 0, item->width(), item->height()));
|
|
const double l = r.left() * dpr, t = r.top() * dpr;
|
|
const double rt = r.right() * dpr, b = r.bottom() * dpr;
|
|
|
|
const bool offL = isOff(l, tol), offT = isOff(t, tol);
|
|
const bool offR = !isText && isOff(rt, tol);
|
|
const bool offB = !isText && isOff(b, tol);
|
|
if (!(offL || offT || offR || offB)) {
|
|
return;
|
|
}
|
|
|
|
const bool own = isOff(item->x() * dpr, tol) ||
|
|
isOff(item->y() * dpr, tol) ||
|
|
(!isText && (isOff(item->width() * dpr, tol) ||
|
|
isOff(item->height() * dpr, tol)));
|
|
if (!own && !o.includeInherited) {
|
|
return;
|
|
}
|
|
|
|
const Source src = sourceOf(item);
|
|
out << src.file;
|
|
if (src.line > 0) {
|
|
out << ':' << src.line << ':' << src.column;
|
|
}
|
|
out << ": " << label(item) << " unaligned ("
|
|
<< (own ? "own" : "inherited") << ")\n"
|
|
<< " device px: left=" << num(l, offL) << " top=" << num(t, offT)
|
|
<< " right=" << num(rt, offR) << " bottom=" << num(b, offB) << "\n"
|
|
<< " local: x=" << item->x() << " y=" << item->y()
|
|
<< " w=" << item->width() << " h=" << item->height() << "\n"
|
|
<< " path: " << ancestry(item) << "\n";
|
|
++count;
|
|
});
|
|
return count;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
struct ScreenReport {
|
|
double dpr = 1.0;
|
|
int windows = 0;
|
|
int count = 0;
|
|
QString body;
|
|
};
|
|
|
|
int run(const Options& options) {
|
|
QMap<QString, ScreenReport> reports;
|
|
|
|
for (QWindow* w : QGuiApplication::allWindows()) {
|
|
auto* qw = qobject_cast<QQuickWindow*>(w);
|
|
if (!qw || !qw->isVisible() || !qw->contentItem()) {
|
|
continue;
|
|
}
|
|
|
|
const QString name = qw->screen() ? qw->screen()->name()
|
|
: QStringLiteral("?");
|
|
ScreenReport& report = reports[name];
|
|
report.dpr = qw->effectiveDevicePixelRatio();
|
|
++report.windows;
|
|
|
|
QString windowBody;
|
|
QTextStream stream(&windowBody);
|
|
report.count += auditWindow(qw, options, stream);
|
|
stream.flush();
|
|
report.body += windowBody;
|
|
}
|
|
|
|
QFile file(options.outputPath);
|
|
if (!file.open(
|
|
QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
|
|
qWarning() << "AlignAudit: cannot open" << options.outputPath;
|
|
return -1;
|
|
}
|
|
QTextStream out(&file);
|
|
|
|
int total = 0;
|
|
for (const ScreenReport& r : std::as_const(reports)) {
|
|
total += r.count;
|
|
}
|
|
|
|
out << "# align audit "
|
|
<< QDateTime::currentDateTime().toString(Qt::ISODate)
|
|
<< " ('*' marks the off edges, values are device pixels)\n"
|
|
<< "# total: " << total << "\n\n";
|
|
|
|
for (auto it = reports.cbegin(); it != reports.cend(); ++it) {
|
|
out << "# screen " << it.key() << " dpr=" << it.value().dpr
|
|
<< " windows=" << it.value().windows
|
|
<< " offenders=" << it.value().count << "\n"
|
|
<< it.value().body << "\n";
|
|
}
|
|
return total;
|
|
}
|
|
|
|
void runLater(int delayMs, const Options& options) {
|
|
QTimer::singleShot(delayMs, qApp, [options] { run(options); });
|
|
}
|
|
|
|
} // namespace ZShell
|