#include "zutils.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "util/metaenum.hpp" Q_LOGGING_CATEGORY(lcZUtils, "ZShell.cutils", QtInfoMsg) namespace ZShell { void ZUtils::saveItem(QQuickItem* target, const QUrl& path) { this->saveItem(target, path, QRect(), QJSValue(), QJSValue()); } void ZUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect) { this->saveItem(target, path, rect, QJSValue(), QJSValue()); } void ZUtils::saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved) { this->saveItem(target, path, QRect(), onSaved, QJSValue()); } void ZUtils::saveItem( QQuickItem* target, const QUrl& path, QJSValue onSaved, QJSValue onFailed) { this->saveItem(target, path, QRect(), onSaved, onFailed); } void ZUtils::saveItem( QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved) { this->saveItem(target, path, rect, onSaved, QJSValue()); } void ZUtils::saveItem( QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed) { if (!target) { qCWarning(lcZUtils) << "saveItem: a target is required"; return; } if (!path.isLocalFile()) { qCWarning(lcZUtils) << "saveItem:" << path << "is not a local file"; return; } if (!target->window()) { qCWarning(lcZUtils) << "saveItem: unable to save target" << target << "without a window"; return; } auto scaledRect = rect; const qreal scale = target->window()->devicePixelRatio(); if (rect.isValid() && !qFuzzyCompare(scale + 1.0, 2.0)) { scaledRect = QRectF( rect.left() * scale, rect.top() * scale, rect.width() * scale, rect.height() * scale) .toRect(); } const QSharedPointer grabResult = target->grabToImage(); QObject::connect( grabResult.data(), &QQuickItemGrabResult::ready, this, [grabResult, scaledRect, path, onSaved, onFailed, this]() { const auto future = QtConcurrent::run([=]() { QImage image = grabResult->image(); if (scaledRect.isValid()) { image = image.copy(scaledRect); } const QString file = path.toLocalFile(); const QString parent = QFileInfo(file).absolutePath(); return QDir().mkpath(parent) && image.save(file); }); auto* watcher = new QFutureWatcher(this); auto* engine = qmlEngine(this); QObject::connect( watcher, &QFutureWatcher::finished, this, [=]() { if (watcher->result()) { if (onSaved.isCallable()) { QJSValueList args = {QJSValue(path.toLocalFile())}; if (engine) { args << engine->toScriptValue( QVariant::fromValue(path)); } onSaved.call(args); } } else { qCWarning(lcZUtils) << "saveItem: failed to save" << path; if (onFailed.isCallable()) { if (engine) { onFailed.call({engine->toScriptValue( QVariant::fromValue(path))}); } else { onFailed.call(); } } } watcher->deleteLater(); }); watcher->setFuture(future); }); } bool ZUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) { if (!source.isLocalFile()) { qCWarning(lcZUtils) << "copyFile: source" << source << "is not a local file"; return false; } if (!target.isLocalFile()) { qCWarning(lcZUtils) << "copyFile: target" << target << "is not a local file"; return false; } if (overwrite && QFile::exists(target.toLocalFile())) { if (!QFile::remove(target.toLocalFile())) { qCWarning(lcZUtils) << "copyFile: overwrite was specified but failed to remove" << target.toLocalFile(); return false; } } return QFile::copy(source.toLocalFile(), target.toLocalFile()); } bool ZUtils::deleteFile(const QUrl& path) { if (!path.isLocalFile()) { qCWarning(lcZUtils) << "deleteFile: path" << path << "is not a local file"; return false; } return QFile::remove(path.toLocalFile()); } QString ZUtils::toLocalFile(const QUrl& url) { if (!url.isLocalFile()) { qCWarning(lcZUtils) << "toLocalFile: given url is not a local file" << url; return QString(); } return url.toLocalFile(); } qreal ZUtils::clamp(qreal value, qreal min, qreal max) { return qBound(min, value, max); } QString ZUtils::enumToString( QObject* target, const QString& property, const QVariant& value) { if (!target) { qCWarning(lcZUtils) << "enumToString: a target is required"; return {}; } const auto* meta = target->metaObject(); const auto index = meta->indexOfProperty(property.toUtf8().constData()); if (index < 0) { qCWarning(lcZUtils) << "enumToString:" << target << "has no property" << property; return {}; } const auto prop = meta->property(index); const auto metaEnum = prop.isEnumType() ? prop.enumerator() : util::metaEnumFor(prop.metaType()); if (!metaEnum.isValid() || metaEnum.is64Bit()) { qCWarning(lcZUtils) << "enumToString: property" << property << "of" << target << "is not a supported enum"; return {}; } const auto val = value.isValid() ? value : prop.read(target); const auto* key = util::enumKeyFor(metaEnum, val); if (!key) { qCWarning( lcZUtils, "enumToString: no enumerator of %s::%s has the value %lld", metaEnum.scope(), metaEnum.name(), val.toLongLong()); return {}; } return QString::fromUtf8(key); } namespace { template QQuickItem* findChildDfs(QQuickItem* root, Predicate&& match) { const auto children = root->childItems(); for (QQuickItem* const child : children) { if (match(child)) { return child; } if (QQuickItem* const found = findChildDfs(child, match)) { return found; } } return nullptr; } template void findChildrenDfs( QQuickItem* root, Predicate&& match, QList& out) { const auto children = root->childItems(); for (QQuickItem* const child : children) { if (match(child)) { out.append(child); } findChildrenDfs(child, match, out); } } } // namespace QQuickItem* ZUtils::findChild(QQuickItem* root, const QString& name) { if (!root) { return nullptr; } return findChildDfs(root, [&name](const QQuickItem* item) { return item->objectName() == name; }); } QList ZUtils::findChildren(QQuickItem* root, const QString& name) { QList children; if (root) { findChildrenDfs( root, [&name](const QQuickItem* item) { return item->objectName() == name; }, children); } return children; } QList ZUtils::findChildrenMatching( QQuickItem* root, const QString& pattern) { QList children; if (root) { const QRegularExpression re(pattern); findChildrenDfs( root, [&re](const QQuickItem* item) { return re.match(item->objectName()).hasMatch(); }, children); } return children; } #ifndef ZSHELL_VERSION #define ZSHELL_VERSION "" #endif QString ZUtils::version() const { return QStringLiteral(ZSHELL_VERSION); } QString ZUtils::qtVersion() const { return QStringLiteral(QT_VERSION_STR); } QString ZUtils::gitRevision() { #ifdef GIT_REVISION return QStringLiteral(GIT_REVISION); #else return QString(); #endif } QString ZUtils::readTextFile(const QString& path) { QFile file(path); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return QString(); } return QString::fromUtf8(file.readAll()); } bool ZUtils::writeTextFile(const QString& path, const QString& text) { const QFileInfo info(path); if (!QDir().mkpath(info.absolutePath())) { qCWarning(lcZUtils) << "Failed to create directory for" << path; return false; } QFile file(path); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qCWarning(lcZUtils) << "Failed to open" << path << "for writing"; return false; } return file.write(text.toUtf8()) >= 0; } QStringList ZUtils::listFiles(const QString& dir, const QString& suffix) { QStringList out; QDirIterator it(dir, QDirIterator::Subdirectories); while (it.hasNext()) { const QString path = it.next(); if (it.fileInfo().isFile() && path.endsWith(suffix)) { out.append(path); } } return out; } qreal ZUtils::dprFor(QQuickItem* item) { if (item) { if (const QQuickWindow* window = item->window()) { return window->effectiveDevicePixelRatio(); } } if (const QScreen* screen = QGuiApplication::primaryScreen()) { return screen->devicePixelRatio(); } return 1.0; } namespace { // Per-axis centering detected from the item's own state. Anchor lines are // Q_GADGET values with no properties, so centering is read from the anchors' // used-anchor bitmask instead of by evaluating `anchors.verticalCenter`. // Layout alignment comes from the attached Layout object; an item with no // attached object uses Qt's default (AlignLeft | AlignVCenter). struct Centering { bool horizontal = false; bool vertical = false; // anchors.alignWhenCentered: false makes Qt round half-sizes in logical px // (y = qRound(parentSize/2) - qRound(size/2)) instead of dividing exactly. bool alignWhenCentered = true; QString source; }; Centering detectCentering(QQuickItem* item) { Centering c; QQuickAnchors* anchors = QQuickItemPrivate::get(item)->_anchors; if (anchors) { const QQuickAnchorsPrivate* ad = QQuickAnchorsPrivate::get(anchors); c.alignWhenCentered = ad->centerAligned; if (ad->usedAnchors & QQuickAnchors::HCenterAnchor) { c.horizontal = true; c.source = QStringLiteral("hCenter"); } if (ad->usedAnchors & QQuickAnchors::VCenterAnchor) { c.vertical = true; c.source += (c.source.isEmpty() ? "" : " ") + QStringLiteral("vCenter"); } } QQuickItem* parent = item->parentItem(); if (parent && parent->inherits("QQuickLayout")) { QQuickLayoutAttached* info = attachedLayoutObject(item, false); const Qt::Alignment flags = info ? info->alignment() : Qt::Alignment(); // Filling an axis means the layout stretches that axis, so no centering. if (info && !info->fillWidth() && (flags & Qt::AlignHCenter)) { c.horizontal = true; c.source += (c.source.isEmpty() ? "" : " ") + QStringLiteral("Layout.HCenter"); } const bool defaultVCenter = flags == 0 || (flags & Qt::AlignVCenter); if ((!info || !info->fillHeight()) && defaultVCenter && !(flags & (Qt::AlignTop | Qt::AlignBottom))) { c.vertical = true; c.source += (c.source.isEmpty() ? "" : " ") + QStringLiteral("Layout.VCenter"); } } return c; } // Parity (mod 2) of an item's whole-device-pixel size; no item = even. int devParity(const QQuickItem* it, qreal dpr, bool horizontal) { if (!it) return 0; const qint64 dev = std::llround((horizontal ? it->width() : it->height()) * dpr); return static_cast(dev % 2); } // Snap a size to a whole device-pixel count whose parity matches `parity` // (the parent's device-size parity). An item centered by *exact* division // (Layout alignment, or anchors with alignWhenCentered: true) lands on whole // device pixels exactly when its device size shares the parent's parity, // since edges = parentLeft + (parentSize - size) / 2. qreal snapToParity(qreal value, qreal dpr, int parity, int mode) { const double exact = value * dpr; qint64 n = mode > 0 ? std::llround(std::ceil(exact - 1e-6)) : mode < 0 ? std::llround(std::floor(exact + 1e-6)) : std::llround(exact); if (static_cast(n % 2) != parity) { if (mode > 0) { ++n; } else if (mode < 0) { --n; } else { // nearest: prefer the side closer to exact; exact ties go up n = (exact >= static_cast(n)) ? n + 1 : n - 1; } } if (n < 0) n = 0; return static_cast(n) / dpr; } // qRound() semantics: round half away from zero. qint64 roundAway(double value) { return value >= 0.0 ? qint64(value + 0.5) : qint64(value - 0.5); } // Snap a size for an item centered by anchors with `alignWhenCentered: false`. // Qt resolves that as `y = qRound(parentSize / 2) + parentPos - size / 2` // (qquickanchors.cpp: the *anchor target* rounds its half, the anchored item // divides exactly), so with the parent's half at a whole device offset the // child's device size must be even; if the parent's half sits on a half device // pixel, the child's device size must be odd instead. qreal snapCenteredSize(qreal value, qreal dpr, qint64 parentHalfRounded, int mode) { const double exact = value * dpr; const double anchorDev = double(parentHalfRounded) * dpr; const bool anchorWhole = std::fabs(anchorDev - std::round(anchorDev)) < 1e-6; const qint64 parity = anchorWhole ? 0 : 1; qint64 base = mode > 0 ? std::llround(std::ceil(exact - 1e-6)) : mode < 0 ? std::llround(std::floor(exact + 1e-6)) : std::llround(exact); for (qint64 step = 0; step < 8; ++step) { if (base + step >= 0 && (base + step) % 2 == parity) return qreal(base + step) / dpr; if (step > 0 && base - step >= 0 && (base - step) % 2 == parity) return qreal(base - step) / dpr; } return qreal(std::max(0, base)) / dpr; } // Snap a size along `horizontal` for an item whose placement on that axis is // centered. Anchor-centered items follow Qt's rounding (`alignWhenCentered`); // anything else only needs the parent's device-size parity. qreal snapCenteredAxis( qreal value, QQuickItem* item, qreal dpr, bool horizontal, const Centering& centering, int mode) { QQuickItem* parent = item->parentItem(); if (!centering.alignWhenCentered && parent) { const qreal parentSize = horizontal ? parent->width() : parent->height(); return snapCenteredSize(value, dpr, roundAway(parentSize / 2.0), mode); } return snapToParity(value, dpr, devParity(parent, dpr, horizontal), mode); } } // namespace qreal ZUtils::snap(qreal value, QQuickItem* item) { const qreal dpr = dprFor(item); if (item) { const Centering c = detectCentering(item); if (c.horizontal) return snapCenteredAxis(value, item, dpr, true, c, 0); if (c.vertical) return snapCenteredAxis(value, item, dpr, false, c, 0); } return std::round(value * dpr) / dpr; } qreal ZUtils::snapCeil(qreal value, QQuickItem* item) { const qreal dpr = dprFor(item); if (item) { const Centering c = detectCentering(item); if (c.horizontal) return snapCenteredAxis(value, item, dpr, true, c, 1); if (c.vertical) return snapCenteredAxis(value, item, dpr, false, c, 1); } return std::ceil(value * dpr - 1e-6) / dpr; } qreal ZUtils::snapFloor(qreal value, QQuickItem* item) { const qreal dpr = dprFor(item); if (item) { const Centering c = detectCentering(item); if (c.horizontal) return snapCenteredAxis(value, item, dpr, true, c, -1); if (c.vertical) return snapCenteredAxis(value, item, dpr, false, c, -1); } return std::floor(value * dpr + 1e-6) / dpr; } bool ZUtils::logPlacement(QQuickItem* item, const QString& tag) { if (!item) { qWarning() << "logPlacement: null item"; return false; } constexpr double tol = 1e-3; const auto isOff = [](double v) { return std::abs(v - std::round(v)) > tol; }; const auto fmt = [&](double v) { return QString::number(v, 'f', 2) + (isOff(v) ? QStringLiteral("*") : QString()); }; // Centering is detected from the item: anchors.centerIn, the individual // center anchor lines, and Layout alignment. const Centering c = detectCentering(item); const bool centeredH = c.horizontal; const bool centeredV = c.vertical; const QString& centerSrc = c.source; const double dpr = dprFor(item); const QRectF r = item->mapRectToScene(QRectF(0, 0, item->width(), item->height())); const double l = r.left() * dpr; const double t = r.top() * dpr; const double rt = r.right() * dpr; const double b = r.bottom() * dpr; // Same rule as the audit: a Text's fractional implicit size doesn't blur anything. const bool isText = item->inherits("QQuickText"); // Centered rule per axis: whole device size, and the center on the grid // with parity matching that size (even size -> integer center, odd -> // pixel center at .5). That is what makes both edges land on whole device // pixels when the center is divided exactly (`alignWhenCentered: true`). // With `alignWhenCentered: false` Qt rounds the anchor target's half // instead of dividing, so the edges are what matter. const auto centeredAxisOk = [&](double lo, double hi) { const double size = hi - lo; if (isOff(size)) return false; if (!c.alignWhenCentered) return !isOff(lo) && !isOff(hi); const double center = (lo + hi) / 2.0; const double parity = (std::llround(size) % 2 == 0) ? 0.0 : 0.5; double err = std::fmod(center - parity, 1.0); if (err < 0) err += 1.0; return std::min(err, 1.0 - err) <= tol; }; bool aligned; if (centeredH && centeredV) { aligned = centeredAxisOk(l, rt) && (isText || centeredAxisOk(t, b)); } else if (centeredH) { aligned = centeredAxisOk(l, rt) && !isOff(t) && (isText || !isOff(b)); } else if (centeredV) { aligned = !isOff(l) && centeredAxisOk(t, b) && (isText || !isOff(rt)); } else { aligned = !isOff(l) && !isOff(t) && (isText || (!isOff(rt) && !isOff(b))); } QString file; if (const QQmlContext* ctx = qmlContext(item)) { const QUrl url = ctx->baseUrl(); file = url.isLocalFile() ? url.toLocalFile() : url.toString(); } qInfo().noquote() << (aligned ? "[ALIGNED] " : "[UNALIGNED]") << (tag.isEmpty() ? QString::fromUtf8(item->metaObject()->className()) : tag) << "\n device px: left=" << fmt(l) << " top=" << fmt(t) << " right=" << fmt(rt) << " bottom=" << fmt(b) << (isText ? " (right/bottom ignored: Text)" : "") << (!centerSrc.isEmpty() ? " (centered: " + centerSrc + ")" : QString()) << (centeredH || centeredV ? QString("\n center: x=%1 y=%2 size=%3 x %4") .arg(QString::number((l + rt) / 2, 'f', 2)) .arg(QString::number((t + b) / 2, 'f', 2)) .arg(QString::number(rt - l, 'f', 2)) .arg(QString::number(b - t, 'f', 2)) : QString()) << "\n size: " << fmt(item->width() * dpr) << " x " << fmt(item->height() * dpr) << "\n local: x=" << item->x() << " y=" << item->y() << " w=" << item->width() << " h=" << item->height() << "\n dpr=" << dpr << (item->window() ? "" : " (NO WINDOW, using fallback dpr)") << (file.isEmpty() ? QString() : "\n file: " + file); return aligned; } bool ZUtils::logCurvePlacement( QQuickItem* item, const QList& samples, qreal lineWidth, const QString& tag) { if (!item) { qWarning() << "logCurvePlacement: null item"; return false; } if (samples.size() < 4 || samples.size() % 2 != 0) { qWarning() << "logCurvePlacement: need flat x,y pairs (>= 2 points)"; return false; } constexpr double tol = 1e-3; const double dpr = dprFor(item); const int devW = static_cast( std::max(1, std::llround(lineWidth * dpr))); const double half = devW / 2.0; qsizetype offCount = 0; qsizetype sloped = 0; double worst = 0.0; double worstX = 0.0; double worstY = 0.0; double worstEdge = 0.0; int worstSide = 0; // -1 top edge, +1 bottom edge for (qsizetype i = 0; i < samples.size(); i += 2) { const double x = samples[i]; const double y = samples[i + 1]; // A sloped stroke's edge is never horizontal in device space, so // only locally-flat samples can (and must) sit on pixel boundaries const double yPrev = i >= 2 ? samples[i - 1] : y; const double yNext = i + 3 < samples.size() ? samples[i + 3] : y; if (std::abs(yNext - yPrev) > tol) { ++sloped; continue; } const double yc = y * dpr; const double top = yc - half; const double bottom = yc + half; for (int side = -1; side <= 1; side += 2) { const double edge = side < 0 ? top : bottom; const double dev = std::abs(edge - std::round(edge)); if (dev > tol) { ++offCount; if (dev > worst) { worst = dev; worstX = x; worstY = y; worstEdge = edge; worstSide = side; } } } } const qsizetype total = samples.size() / 2; const qsizetype flat = total - sloped; const bool aligned = offCount == 0; qInfo().noquote() << (aligned ? "[CURVE ALIGNED] " : "[CURVE UNALIGNED]") << (tag.isEmpty() ? QString::fromUtf8(item->metaObject()->className()) : tag) << "\n samples: " << total << " points (" << flat << " flat, " << sloped << " sloped ignored), dpr=" << dpr << ", stroke width=" << lineWidth << " logical = " << devW << " device px" << "\n edges: " << offCount << " of " << flat * 2 << " flat stroke edges off the device grid"; if (!aligned) { qInfo().noquote() << " worst: deviation=" << QString::number(worst, 'f', 3) << " device px at logical x=" << QString::number(worstX, 'f', 2) << " y=" << QString::number(worstY, 'f', 2) << " (" << (worstSide < 0 ? "top" : "bottom") << " edge device=" << QString::number(worstEdge, 'f', 3) << ")"; } return aligned; } QList ZUtils::distribute( qreal total, const QList& preferred, QQuickItem* item) { const qsizetype n = preferred.size(); QList out(n, 0.0); if (n == 0) { return out; } const qreal dpr = dprFor(item); const qint64 totalPx = std::max(0, std::llround(total * dpr)); QList px; px.reserve(n); qint64 sum = 0; for (const qreal p : preferred) { const qint64 v = std::max(0, std::llround(std::ceil(p * dpr - 1e-6))); px.append(v); sum += v; } const qint64 left = totalPx - sum; if (left >= 0) { // Extra space: split evenly, remainder pixels to the first items. const qint64 base = left / n; const qint64 rem = left % n; for (qsizetype i = 0; i < n; ++i) { px[i] += base + (i < rem ? 1 : 0); } } else if (sum > 0) { // Not enough space: shrink proportionally, then hand out the rounding leftovers. qint64 assigned = 0; for (qsizetype i = 0; i < n; ++i) { px[i] = static_cast(std::floor( static_cast(px[i]) * static_cast(totalPx) / static_cast(sum))); assigned += px[i]; } for (qsizetype i = 0; assigned < totalPx; i = (i + 1) % n, ++assigned) { ++px[i]; } } for (qsizetype i = 0; i < n; ++i) { out[i] = static_cast(px[i]) / dpr; } return out; } } // namespace ZShell