#include "alignaudit.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef ALIGN_AUDIT_USE_QML_PRIVATE #include #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& 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 reports; for (QWindow* w : QGuiApplication::allWindows()) { auto* qw = qobject_cast(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