765 lines
22 KiB
C++
765 lines
22 KiB
C++
#include "zutils.hpp"
|
|
|
|
#include <QtConcurrent/qtconcurrentrun.h>
|
|
#include <QtQuick/private/qquickanchors_p_p.h>
|
|
#include <QtQuickLayouts/private/qquicklayout_p.h>
|
|
#include <QtQuick/qquickitemgrabresult.h>
|
|
#include <QtQuick/qquickwindow.h>
|
|
#include <QQmlContext>
|
|
#include <QQmlExpression>
|
|
#include <QDebug>
|
|
#include <QUrl>
|
|
#include <qcontainerfwd.h>
|
|
#include <qdir.h>
|
|
#include <qdiriterator.h>
|
|
#include <qfileinfo.h>
|
|
#include <qfuturewatcher.h>
|
|
#include <qjsprimitivevalue.h>
|
|
#include <qloggingcategory.h>
|
|
#include <qregularexpression.h>
|
|
#include <qqmlengine.h>
|
|
#include <qfile.h>
|
|
|
|
#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<const QQuickItemGrabResult> 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<bool>(this);
|
|
auto* engine = qmlEngine(this);
|
|
|
|
QObject::connect(
|
|
watcher, &QFutureWatcher<bool>::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 <typename Predicate>
|
|
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 <typename Predicate>
|
|
void findChildrenDfs(
|
|
QQuickItem* root, Predicate&& match, QList<QQuickItem*>& 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<QQuickItem*> ZUtils::findChildren(QQuickItem* root, const QString& name) {
|
|
QList<QQuickItem*> children;
|
|
if (root) {
|
|
findChildrenDfs(
|
|
root,
|
|
[&name](const QQuickItem* item) {
|
|
return item->objectName() == name;
|
|
},
|
|
children);
|
|
}
|
|
return children;
|
|
}
|
|
|
|
QList<QQuickItem*> ZUtils::findChildrenMatching(
|
|
QQuickItem* root, const QString& pattern) {
|
|
QList<QQuickItem*> 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<int>(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<int>(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<double>(n)) ? n + 1 : n - 1;
|
|
}
|
|
}
|
|
if (n < 0)
|
|
n = 0;
|
|
return static_cast<qreal>(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<qint64>(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<qreal>& 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<int>(
|
|
std::max<qint64>(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<qreal> ZUtils::distribute(
|
|
qreal total, const QList<qreal>& preferred, QQuickItem* item) {
|
|
const qsizetype n = preferred.size();
|
|
QList<qreal> out(n, 0.0);
|
|
if (n == 0) {
|
|
return out;
|
|
}
|
|
|
|
const qreal dpr = dprFor(item);
|
|
const qint64 totalPx = std::max<qint64>(0, std::llround(total * dpr));
|
|
|
|
QList<qint64> px;
|
|
px.reserve(n);
|
|
qint64 sum = 0;
|
|
for (const qreal p : preferred) {
|
|
const qint64 v =
|
|
std::max<qint64>(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<qint64>(std::floor(
|
|
static_cast<double>(px[i]) * static_cast<double>(totalPx) /
|
|
static_cast<double>(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<qreal>(px[i]) / dpr;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
} // namespace ZShell
|