feat: initial commit for pixel-snapping components and helpers
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user