feat: initial commit for pixel-snapping components and helpers
This commit is contained in:
@@ -9,12 +9,19 @@ qml_module(ZShell
|
||||
toaster.hpp toaster.cpp
|
||||
qalculator.hpp qalculator.cpp
|
||||
zutils.hpp zutils.cpp
|
||||
alignaudit.hpp alignaudit.cpp
|
||||
alignauditor.hpp alignauditor.cpp
|
||||
LIBRARIES
|
||||
Qt::Gui
|
||||
Qt::Quick
|
||||
Qt::Concurrent
|
||||
Qt::Sql
|
||||
Qt::DBus
|
||||
Qt6::Gui
|
||||
Qt6::Quick
|
||||
Qt6::QuickPrivate
|
||||
Qt6::QuickLayouts
|
||||
Qt6::QuickLayoutsPrivate
|
||||
Qt6::QmlPrivate
|
||||
Qt6::CorePrivate
|
||||
Qt6::Concurrent
|
||||
Qt6::Sql
|
||||
Qt6::DBus
|
||||
PkgConfig::Qalculate
|
||||
zshell-util
|
||||
)
|
||||
@@ -24,6 +31,9 @@ target_compile_definitions(ZShell PRIVATE
|
||||
GIT_REVISION="${GIT_REVISION}"
|
||||
)
|
||||
|
||||
target_compile_definitions(ZShell PRIVATE ALIGN_AUDIT_USE_QML_PRIVATE)
|
||||
|
||||
|
||||
# Node macros expand to Q_OBJECT, automoc can't see it otherwise
|
||||
list(APPEND CMAKE_AUTOMOC_MACRO_NAMES
|
||||
CONFIG_NODE
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
qml_module(ZShell-components
|
||||
URI ZShell.Components
|
||||
DEPENDENCIES QtQuick.Layouts
|
||||
SOURCES
|
||||
lazylistview.hpp lazylistview.cpp
|
||||
chatlistview.hpp chatlistview.cpp
|
||||
@@ -12,10 +13,14 @@ qml_module(ZShell-components
|
||||
sparklinesceneitem.hpp sparklinesceneitem.cpp
|
||||
roundedmask.hpp roundedmask.cpp
|
||||
roundedclip.hpp roundedclip.cpp
|
||||
snaplayout.hpp snaplayout.cpp
|
||||
wheelinverter.hpp
|
||||
LIBRARIES
|
||||
Qt::Quick
|
||||
Qt::QuickPrivate
|
||||
Qt::QuickLayouts
|
||||
Qt::QuickLayoutsPrivate
|
||||
Qt::GuiPrivate
|
||||
Qt::CanvasPainter
|
||||
ZShell-internal
|
||||
)
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
#include "snaplayout.hpp"
|
||||
|
||||
#include <QLoggingCategory>
|
||||
#include <QQuickWindow>
|
||||
#include <QScopeGuard>
|
||||
#include <QtGui/private/qfont_p.h>
|
||||
#include <cmath>
|
||||
|
||||
Q_LOGGING_CATEGORY(lcQuickLayouts, "qt.quick.layouts")
|
||||
|
||||
namespace ZShell::components {
|
||||
namespace {
|
||||
|
||||
qint64 originResidual(qreal logical, qreal dpr) {
|
||||
const qreal device = logical * dpr;
|
||||
return std::llround((device - std::floor(device)) * 1024.0) % 1024;
|
||||
}
|
||||
|
||||
QPointF absoluteOrigin(const QQuickItem* item) {
|
||||
qreal x = 0.0;
|
||||
qreal y = 0.0;
|
||||
for (const QQuickItem* it = item; it; it = it->parentItem()) {
|
||||
x += it->x();
|
||||
y += it->y();
|
||||
}
|
||||
return QPointF(x, y);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
qreal SnapLayoutStyleInfo::spacing(Qt::Orientation /*orientation*/) const {
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_QNX)
|
||||
qreal spacing = 8.0;
|
||||
#else
|
||||
qreal spacing = 5.0;
|
||||
#endif
|
||||
#if !defined(Q_OS_MACOS) && !defined(Q_OS_HARMONY)
|
||||
spacing = qRound(spacing * (qreal(qt_defaultDpiX()) / 96.0));
|
||||
#endif
|
||||
return spacing;
|
||||
}
|
||||
|
||||
qreal SnapLayoutStyleInfo::windowMargin(Qt::Orientation /*orientation*/) const {
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool SnapLayoutStyleInfo::hasChangedCore() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
SnapGridLayoutItem::SnapGridLayoutItem(
|
||||
QQuickItem* item,
|
||||
int row,
|
||||
int column,
|
||||
int rowSpan,
|
||||
int columnSpan,
|
||||
Qt::Alignment alignment,
|
||||
SnapLayout* layout)
|
||||
: QQuickGridLayoutItem(item, row, column, rowSpan, columnSpan, alignment)
|
||||
, m_layout(layout) {}
|
||||
|
||||
void SnapGridLayoutItem::setGeometry(const QRectF& rect) {
|
||||
QQuickLayoutAttached* info = attachedLayoutObject(m_item, false);
|
||||
QRectF placed = info ? rect.marginsRemoved(info->effectiveQMargins())
|
||||
: rect;
|
||||
|
||||
const QQuickWindow* win = m_item->window();
|
||||
const qreal dpr = win ? win->effectiveDevicePixelRatio() : 1.0;
|
||||
|
||||
if (dpr > 0.0) {
|
||||
const QPointF origin = absoluteOrigin(m_layout);
|
||||
const qreal left = (origin.x() + placed.x()) * dpr;
|
||||
const qreal top = (origin.y() + placed.y()) * dpr;
|
||||
const qreal right = (origin.x() + placed.right()) * dpr;
|
||||
const qreal bottom = (origin.y() + placed.bottom()) * dpr;
|
||||
|
||||
const qint64 nearX = std::llround(left);
|
||||
const qint64 nearY = std::llround(top);
|
||||
const qint64 farX = std::llround(right);
|
||||
const qint64 farY = std::llround(bottom);
|
||||
|
||||
placed = QRectF(
|
||||
qreal(nearX) / dpr - origin.x(),
|
||||
qreal(nearY) / dpr - origin.y(),
|
||||
qreal(farX - nearX) / dpr,
|
||||
qreal(farY - nearY) / dpr);
|
||||
}
|
||||
|
||||
const QSizeF oldSize(m_item->width(), m_item->height());
|
||||
const QSizeF newSize = placed.size();
|
||||
|
||||
m_item->setPosition(placed.topLeft());
|
||||
if (newSize == oldSize) {
|
||||
if (QQuickLayout* lay = qobject_cast<QQuickLayout*>(m_item)) {
|
||||
if (lay->invalidatedArrangement()) lay->rearrange(newSize);
|
||||
}
|
||||
} else {
|
||||
m_item->setSize(newSize);
|
||||
}
|
||||
}
|
||||
|
||||
void SnapLayoutPrivate::mirrorChange() {
|
||||
Q_Q(SnapLayout);
|
||||
q->invalidate();
|
||||
}
|
||||
|
||||
SnapLayout::SnapLayout(QQuickItem* parent)
|
||||
: QQuickLayout(*new SnapLayoutPrivate, parent) {
|
||||
d_func()->styleInfo = new SnapLayoutStyleInfo;
|
||||
}
|
||||
|
||||
SnapLayout::~SnapLayout() {
|
||||
Q_D(SnapLayout);
|
||||
clearAncestorListeners();
|
||||
deactivateRecur();
|
||||
delete d->styleInfo;
|
||||
}
|
||||
|
||||
void SnapLayout::componentComplete() {
|
||||
QQuickLayout::componentComplete();
|
||||
|
||||
invalidate();
|
||||
ensureLayoutItemsUpdated(QQuickLayout::ApplySizeHints);
|
||||
|
||||
if (qobject_cast<QQuickLayout*>(parentItem())) return;
|
||||
rearrange(QSizeF(width(), height()));
|
||||
}
|
||||
|
||||
void SnapLayout::itemChange(ItemChange change, const ItemChangeData& value) {
|
||||
QQuickLayout::itemChange(change, value);
|
||||
if (change == ItemDevicePixelRatioHasChanged || change == ItemSceneChange)
|
||||
invalidate();
|
||||
}
|
||||
|
||||
QSizeF SnapLayout::sizeHint(Qt::SizeHint whichSizeHint) const {
|
||||
Q_D(const SnapLayout);
|
||||
return d->engine.sizeHint(whichSizeHint, QSizeF(), d->styleInfo);
|
||||
}
|
||||
|
||||
Qt::Orientation SnapLayout::orientation() const {
|
||||
return d_func()->orientation;
|
||||
}
|
||||
|
||||
void SnapLayout::setOrientation(Qt::Orientation orientation) {
|
||||
Q_D(SnapLayout);
|
||||
if (d->orientation == orientation) return;
|
||||
d->orientation = orientation;
|
||||
invalidate();
|
||||
emit orientationChanged();
|
||||
}
|
||||
|
||||
Qt::LayoutDirection SnapLayout::layoutDirection() const {
|
||||
return d_func()->layoutDirection;
|
||||
}
|
||||
|
||||
void SnapLayout::setLayoutDirection(Qt::LayoutDirection direction) {
|
||||
Q_D(SnapLayout);
|
||||
if (d->layoutDirection == direction) return;
|
||||
d->layoutDirection = direction;
|
||||
invalidate();
|
||||
emit layoutDirectionChanged();
|
||||
}
|
||||
|
||||
Qt::LayoutDirection SnapLayout::effectiveLayoutDirection() const {
|
||||
return !isMirrored() == (layoutDirection() == Qt::LeftToRight)
|
||||
? Qt::LeftToRight
|
||||
: Qt::RightToLeft;
|
||||
}
|
||||
|
||||
qreal SnapLayout::spacing() const {
|
||||
Q_D(const SnapLayout);
|
||||
return d->engine.spacing(d->orientation, d->styleInfo);
|
||||
}
|
||||
|
||||
void SnapLayout::setSpacing(qreal space) {
|
||||
Q_D(SnapLayout);
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wfloat-equal"
|
||||
if (std::isnan(space) || spacing() == space) return;
|
||||
#pragma GCC diagnostic pop
|
||||
d->engine.setSpacing(space, Qt::Horizontal | Qt::Vertical);
|
||||
invalidate();
|
||||
emit spacingChanged();
|
||||
}
|
||||
|
||||
bool SnapLayout::uniformCellSizes() const {
|
||||
Q_D(const SnapLayout);
|
||||
return d->engine.uniformCellWidths();
|
||||
}
|
||||
|
||||
void SnapLayout::setUniformCellSizes(bool uniformCellSizes) {
|
||||
Q_D(SnapLayout);
|
||||
if (d->engine.uniformCellHeights() == uniformCellSizes) return;
|
||||
d->engine.setUniformCellWidths(uniformCellSizes);
|
||||
d->engine.setUniformCellHeights(uniformCellSizes);
|
||||
invalidate();
|
||||
emit uniformCellSizesChanged();
|
||||
}
|
||||
|
||||
void SnapLayout::setAlignment(QQuickItem* item, Qt::Alignment align) {
|
||||
Q_D(SnapLayout);
|
||||
if (QQuickGridLayoutItem* layoutItem = d->engine.findLayoutItem(item)) {
|
||||
layoutItem->setAlignment(align);
|
||||
d->engine.invalidate();
|
||||
}
|
||||
maybeSubscribeToBaseLineOffsetChanges(item);
|
||||
}
|
||||
|
||||
void SnapLayout::setStretchFactor(
|
||||
QQuickItem* item, int stretchFactor, Qt::Orientation orient) {
|
||||
Q_D(SnapLayout);
|
||||
if (QQuickGridLayoutItem* layoutItem = d->engine.findLayoutItem(item)) {
|
||||
layoutItem->setStretchFactor(stretchFactor, orient);
|
||||
d->engine.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
void SnapLayout::invalidate(QQuickItem* childItem) {
|
||||
Q_D(SnapLayout);
|
||||
if (!isReady()) return;
|
||||
|
||||
if (childItem) {
|
||||
if (d->rearranging) {
|
||||
if (!d->invalidateAfterRearrange.contains(childItem))
|
||||
d->invalidateAfterRearrange << childItem;
|
||||
return;
|
||||
}
|
||||
if (QQuickGridLayoutItem* layoutItem =
|
||||
d->engine.findLayoutItem(childItem)) {
|
||||
layoutItem->invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
d->engine.invalidate();
|
||||
QQuickLayout::invalidate();
|
||||
|
||||
if (auto* parentLayout = qobject_cast<QQuickLayout*>(parentItem()))
|
||||
parentLayout->invalidate(this);
|
||||
}
|
||||
|
||||
void SnapLayout::updateLayoutItems() {
|
||||
Q_D(SnapLayout);
|
||||
if (!isReady()) return;
|
||||
|
||||
d->engine.deleteItems();
|
||||
insertLayoutItems();
|
||||
}
|
||||
|
||||
QQuickItem* SnapLayout::itemAt(int index) const {
|
||||
Q_D(const SnapLayout);
|
||||
return static_cast<QQuickGridLayoutItem*>(d->engine.itemAt(index))
|
||||
->layoutItem();
|
||||
}
|
||||
|
||||
int SnapLayout::itemCount() const {
|
||||
Q_D(const SnapLayout);
|
||||
return d->engine.itemCount();
|
||||
}
|
||||
|
||||
void SnapLayout::insertLayoutItems() {
|
||||
Q_D(SnapLayout);
|
||||
const auto items = childItems();
|
||||
for (QQuickItem* child : items) {
|
||||
Q_ASSERT(child);
|
||||
checkAnchors(child);
|
||||
|
||||
if (shouldIgnoreItem(child)) continue;
|
||||
QQuickLayoutAttached* info = attachedLayoutObject(child, false);
|
||||
|
||||
Qt::Alignment alignment;
|
||||
int hStretch = -1;
|
||||
int vStretch = -1;
|
||||
bool fillWidth = false;
|
||||
bool fillHeight = false;
|
||||
if (info) {
|
||||
alignment = info->alignment();
|
||||
hStretch = info->horizontalStretchFactor();
|
||||
vStretch = info->verticalStretchFactor();
|
||||
fillWidth = info->fillWidth();
|
||||
fillHeight = info->fillHeight();
|
||||
}
|
||||
|
||||
const int index = d->engine.rowCount(d->orientation);
|
||||
d->engine.insertRow(index, d->orientation);
|
||||
|
||||
int gridRow = 0;
|
||||
int gridColumn = index;
|
||||
if (d->orientation == Qt::Vertical) qSwap(gridRow, gridColumn);
|
||||
QGridLayoutItem* layoutItem = new SnapGridLayoutItem(
|
||||
child, gridRow, gridColumn, 1, 1, alignment, this);
|
||||
|
||||
if (hStretch >= 0) {
|
||||
if (!fillWidth)
|
||||
qWarning() << "horizontalStretchFactor requires fillWidth to "
|
||||
"also be set to true";
|
||||
layoutItem->setStretchFactor(hStretch, Qt::Horizontal);
|
||||
}
|
||||
if (vStretch >= 0) {
|
||||
if (!fillHeight)
|
||||
qWarning() << "verticalStretchFactor requires fillHeight to "
|
||||
"also be set to true";
|
||||
layoutItem->setStretchFactor(vStretch, Qt::Vertical);
|
||||
}
|
||||
d->engine.insertItem(layoutItem, index);
|
||||
}
|
||||
}
|
||||
|
||||
void SnapLayout::removeGridItem(QGridLayoutItem* gridItem) {
|
||||
Q_D(SnapLayout);
|
||||
const int index = gridItem->firstRow(d->orientation);
|
||||
d->engine.removeItem(gridItem);
|
||||
d->engine.removeRows(index, 1, d->orientation);
|
||||
}
|
||||
|
||||
void SnapLayout::itemDestroyed(QQuickItem* item) {
|
||||
if (!isReady()) return;
|
||||
Q_D(SnapLayout);
|
||||
if (QQuickGridLayoutItem* gridItem = d->engine.findLayoutItem(item)) {
|
||||
removeGridItem(gridItem);
|
||||
delete gridItem;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
void SnapLayout::itemVisibilityChanged(QQuickItem* item) {
|
||||
if (!isReady()) return;
|
||||
invalidate(item);
|
||||
}
|
||||
|
||||
void SnapLayout::itemGeometryChanged(
|
||||
QQuickItem* item, QQuickGeometryChange change, const QRectF& oldGeometry) {
|
||||
Q_UNUSED(item);
|
||||
Q_UNUSED(oldGeometry);
|
||||
if (!isReady() || !change.positionChange()) return;
|
||||
|
||||
Q_D(SnapLayout);
|
||||
const QQuickWindow* win = window();
|
||||
const qreal dpr = win ? win->effectiveDevicePixelRatio() : 0.0;
|
||||
if (dpr <= 0.0) return;
|
||||
|
||||
const QPointF origin = absoluteOrigin(this);
|
||||
if (originResidual(origin.x(), dpr) == d->originResidualX &&
|
||||
originResidual(origin.y(), dpr) == d->originResidualY)
|
||||
return;
|
||||
polish();
|
||||
}
|
||||
|
||||
void SnapLayout::updateAncestorListeners() {
|
||||
Q_D(SnapLayout);
|
||||
QVector<QQuickItem*> current;
|
||||
for (QQuickItem* it = parentItem(); it; it = it->parentItem()) {
|
||||
if (qobject_cast<SnapLayout*>(it)) break;
|
||||
current.append(it);
|
||||
}
|
||||
|
||||
for (const QPointer<QQuickItem>& ancestor :
|
||||
std::as_const(d->ancestorListeners)) {
|
||||
if (ancestor && !current.contains(ancestor.data()))
|
||||
QQuickItemPrivate::get(ancestor.data())
|
||||
->removeItemChangeListener(this, QQuickItemPrivate::Geometry);
|
||||
}
|
||||
for (QQuickItem* ancestor : std::as_const(current)) {
|
||||
if (!d->ancestorListeners.contains(ancestor))
|
||||
QQuickItemPrivate::get(ancestor)->addItemChangeListener(
|
||||
this, QQuickItemPrivate::Geometry);
|
||||
}
|
||||
d->ancestorListeners.clear();
|
||||
d->ancestorListeners.reserve(current.size());
|
||||
for (QQuickItem* ancestor : std::as_const(current))
|
||||
d->ancestorListeners.append(ancestor);
|
||||
}
|
||||
|
||||
void SnapLayout::clearAncestorListeners() {
|
||||
Q_D(SnapLayout);
|
||||
for (const QPointer<QQuickItem>& ancestor :
|
||||
std::as_const(d->ancestorListeners)) {
|
||||
if (ancestor)
|
||||
QQuickItemPrivate::get(ancestor.data())
|
||||
->removeItemChangeListener(this, QQuickItemPrivate::Geometry);
|
||||
}
|
||||
d->ancestorListeners.clear();
|
||||
}
|
||||
|
||||
void SnapLayout::rearrange(const QSizeF& size) {
|
||||
Q_D(SnapLayout);
|
||||
if (!isReady() || !size.isValid()) return;
|
||||
|
||||
const auto refCounter = qScopeGuard([&d] { --(d->recurRearrangeCounter); });
|
||||
if (d->recurRearrangeCounter++ == 2) {
|
||||
qWarning() << "Qt Quick Layouts: Detected recursive rearrange. "
|
||||
"Aborting after two iterations.";
|
||||
return;
|
||||
}
|
||||
|
||||
ensureLayoutItemsUpdated(
|
||||
QQuickLayout::ApplySizeHints | QQuickLayout::Recursive);
|
||||
|
||||
d->rearranging = true;
|
||||
d->engine.setVisualDirection(effectiveLayoutDirection());
|
||||
updateAncestorListeners();
|
||||
|
||||
if (const QQuickWindow* win = window()) {
|
||||
if (const qreal dpr = win->effectiveDevicePixelRatio(); dpr > 0.0) {
|
||||
const QPointF origin = absoluteOrigin(this);
|
||||
d->originResidualX = originResidual(origin.x(), dpr);
|
||||
d->originResidualY = originResidual(origin.y(), dpr);
|
||||
}
|
||||
}
|
||||
|
||||
QQuickLayout::rearrange(size);
|
||||
d->engine.setGeometries(QRectF(QPointF(0, 0), size), d->styleInfo);
|
||||
d->rearranging = false;
|
||||
|
||||
for (auto childItem : std::as_const(d->invalidateAfterRearrange))
|
||||
invalidate(childItem);
|
||||
d->invalidateAfterRearrange.clear();
|
||||
}
|
||||
|
||||
} // namespace ZShell::components
|
||||
@@ -0,0 +1,129 @@
|
||||
#pragma once
|
||||
|
||||
#include <QPointer>
|
||||
#include <QtGui/private/qabstractlayoutstyleinfo_p.h>
|
||||
#include <QtGui/private/qgridlayoutengine_p.h>
|
||||
#include <QtQuickLayouts/private/qquickgridlayoutengine_p.h>
|
||||
#include <QtQuickLayouts/private/qquicklayout_p.h>
|
||||
#include <QtQuick/private/qquickitem_p.h>
|
||||
#include <qqmlintegration.h>
|
||||
|
||||
namespace ZShell::components {
|
||||
|
||||
class SnapLayout;
|
||||
class SnapLayoutPrivate;
|
||||
|
||||
class SnapLayoutStyleInfo : public QAbstractLayoutStyleInfo {
|
||||
public:
|
||||
qreal spacing(Qt::Orientation orientation) const override;
|
||||
qreal windowMargin(Qt::Orientation orientation) const override;
|
||||
bool hasChangedCore() const override;
|
||||
};
|
||||
|
||||
class SnapGridLayoutItem : public QQuickGridLayoutItem {
|
||||
public:
|
||||
SnapGridLayoutItem(
|
||||
QQuickItem* item,
|
||||
int row,
|
||||
int column,
|
||||
int rowSpan,
|
||||
int columnSpan,
|
||||
Qt::Alignment alignment,
|
||||
SnapLayout* layout);
|
||||
|
||||
void setGeometry(const QRectF& rect) override;
|
||||
|
||||
private:
|
||||
SnapLayout* m_layout;
|
||||
};
|
||||
|
||||
class SnapLayout : public QQuickLayout {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(
|
||||
Qt::Orientation orientation READ orientation WRITE setOrientation NOTIFY
|
||||
orientationChanged)
|
||||
Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged)
|
||||
Q_PROPERTY(
|
||||
Qt::LayoutDirection layoutDirection READ layoutDirection WRITE
|
||||
setLayoutDirection NOTIFY layoutDirectionChanged)
|
||||
Q_PROPERTY(
|
||||
bool uniformCellSizes READ uniformCellSizes WRITE setUniformCellSizes
|
||||
NOTIFY uniformCellSizesChanged)
|
||||
|
||||
public:
|
||||
explicit SnapLayout(QQuickItem* parent = nullptr);
|
||||
~SnapLayout() override;
|
||||
|
||||
void componentComplete() override;
|
||||
QSizeF sizeHint(Qt::SizeHint whichSizeHint) const override;
|
||||
void setAlignment(QQuickItem* item, Qt::Alignment align) override;
|
||||
void setStretchFactor(
|
||||
QQuickItem* item, int stretchFactor, Qt::Orientation orient) override;
|
||||
void invalidate(QQuickItem* childItem = nullptr) override;
|
||||
void updateLayoutItems() override;
|
||||
QQuickItem* itemAt(int index) const override;
|
||||
int itemCount() const override;
|
||||
void rearrange(const QSizeF& size) override;
|
||||
|
||||
void itemChange(ItemChange change, const ItemChangeData& value) override;
|
||||
|
||||
void itemDestroyed(QQuickItem* item) override;
|
||||
void itemVisibilityChanged(QQuickItem* item) override;
|
||||
void itemGeometryChanged(
|
||||
QQuickItem* item,
|
||||
QQuickGeometryChange change,
|
||||
const QRectF& oldGeometry) override;
|
||||
|
||||
Qt::Orientation orientation() const;
|
||||
void setOrientation(Qt::Orientation orientation);
|
||||
|
||||
qreal spacing() const;
|
||||
void setSpacing(qreal spacing);
|
||||
|
||||
Qt::LayoutDirection layoutDirection() const;
|
||||
void setLayoutDirection(Qt::LayoutDirection direction);
|
||||
Qt::LayoutDirection effectiveLayoutDirection() const;
|
||||
|
||||
bool uniformCellSizes() const;
|
||||
void setUniformCellSizes(bool uniformCellSizes);
|
||||
|
||||
signals:
|
||||
void orientationChanged();
|
||||
void spacingChanged();
|
||||
void layoutDirectionChanged();
|
||||
void uniformCellSizesChanged();
|
||||
|
||||
private:
|
||||
Q_DECLARE_PRIVATE(SnapLayout)
|
||||
|
||||
void insertLayoutItems();
|
||||
void removeGridItem(QGridLayoutItem* gridItem);
|
||||
|
||||
void updateAncestorListeners();
|
||||
void clearAncestorListeners();
|
||||
};
|
||||
|
||||
class SnapLayoutPrivate : public QQuickLayoutPrivate {
|
||||
Q_DECLARE_PUBLIC(SnapLayout)
|
||||
|
||||
public:
|
||||
SnapLayoutPrivate() = default;
|
||||
|
||||
void mirrorChange() override;
|
||||
|
||||
QQuickGridLayoutEngine engine;
|
||||
SnapLayoutStyleInfo* styleInfo = nullptr;
|
||||
Qt::Orientation orientation = Qt::Horizontal;
|
||||
Qt::LayoutDirection layoutDirection = Qt::LeftToRight;
|
||||
bool rearranging = false;
|
||||
int recurRearrangeCounter = 0;
|
||||
QList<QQuickItem*> invalidateAfterRearrange;
|
||||
|
||||
QVector<QPointer<QQuickItem>> ancestorListeners;
|
||||
qint64 originResidualX = -1;
|
||||
qint64 originResidualY = -1;
|
||||
};
|
||||
|
||||
} // namespace ZShell::components
|
||||
@@ -137,12 +137,12 @@ class FontTokens : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ANONYMOUS
|
||||
|
||||
Q_PROPERTY(FontStyle* headline READ headline CONSTANT FINAL)
|
||||
Q_PROPERTY(FontStyle* title READ title CONSTANT FINAL)
|
||||
Q_PROPERTY(FontStyle* body READ body CONSTANT FINAL)
|
||||
Q_PROPERTY(FontStyle* label READ label CONSTANT FINAL)
|
||||
Q_PROPERTY(FontStyle* mono READ mono CONSTANT FINAL)
|
||||
Q_PROPERTY(IconFontStyle* icon READ icon CONSTANT FINAL)
|
||||
Q_PROPERTY(ZShell::config::FontStyle* headline READ headline CONSTANT FINAL)
|
||||
Q_PROPERTY(ZShell::config::FontStyle* title READ title CONSTANT FINAL)
|
||||
Q_PROPERTY(ZShell::config::FontStyle* body READ body CONSTANT FINAL)
|
||||
Q_PROPERTY(ZShell::config::FontStyle* label READ label CONSTANT FINAL)
|
||||
Q_PROPERTY(ZShell::config::FontStyle* mono READ mono CONSTANT FINAL)
|
||||
Q_PROPERTY(ZShell::config::IconFontStyle* icon READ icon CONSTANT FINAL)
|
||||
Q_PROPERTY(
|
||||
ZShell::config::FontBuilder clock READ clock NOTIFY clockChanged FINAL)
|
||||
Q_PROPERTY(QString workspaces READ workspaces NOTIFY workspacesChanged FINAL)
|
||||
|
||||
@@ -21,15 +21,6 @@ class LauncherActionEntry : public settings::ObjectNode {
|
||||
|
||||
CONFIG_LIST_TYPE(LauncherActionEntry, LauncherActionList)
|
||||
|
||||
class LauncherSizes : public settings::ObjectNode {
|
||||
CONFIG_NODE(LauncherSizes, settings::ObjectNode)
|
||||
|
||||
CONFIG_PROPERTY(int, itemHeight, 50)
|
||||
CONFIG_PROPERTY(int, itemWidth, 600)
|
||||
CONFIG_PROPERTY(int, wallpaperHeight, 200)
|
||||
CONFIG_PROPERTY(int, wallpaperWidth, 280)
|
||||
};
|
||||
|
||||
class UseFuzzy : public settings::ObjectNode {
|
||||
CONFIG_NODE(UseFuzzy, settings::ObjectNode)
|
||||
|
||||
@@ -127,7 +118,6 @@ class Launcher : public settings::ObjectNode {
|
||||
CONFIG_PROPERTY(QStringList, hiddenApps, {})
|
||||
CONFIG_PROPERTY(int, maxAppsShown, 18)
|
||||
CONFIG_PROPERTY(int, maxWallpapers, 7)
|
||||
CONFIG_SUBOBJECT(LauncherSizes, sizes)
|
||||
CONFIG_PROPERTY(QString, specialPrefix, QStringLiteral("@"))
|
||||
CONFIG_SUBOBJECT(UseFuzzy, useFuzzy)
|
||||
CONFIG_PROPERTY(bool, uwsm, true)
|
||||
|
||||
@@ -164,11 +164,21 @@ class DashboardTokens : public settings::ObjectNode {
|
||||
CONFIG_PROPERTY(int, weatherWidth, 250)
|
||||
};
|
||||
|
||||
class LauncherTokens : public settings::ObjectNode {
|
||||
CONFIG_NODE(LauncherTokens, settings::ObjectNode)
|
||||
|
||||
CONFIG_PROPERTY(int, itemHeight, 50)
|
||||
CONFIG_PROPERTY(int, itemWidth, 600)
|
||||
CONFIG_PROPERTY(int, wallpaperHeight, 200)
|
||||
CONFIG_PROPERTY(int, wallpaperWidth, 280)
|
||||
};
|
||||
|
||||
class SizeTokens : public settings::ObjectNode {
|
||||
CONFIG_NODE(SizeTokens, settings::ObjectNode)
|
||||
|
||||
CONFIG_SUBOBJECT(DashboardTokens, dashboard)
|
||||
CONFIG_SUBOBJECT(BarTokens, bar)
|
||||
CONFIG_SUBOBJECT(LauncherTokens, launcher)
|
||||
};
|
||||
|
||||
} // namespace ZShell::config
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
#include <QString>
|
||||
#include <qtmetamacros.h>
|
||||
|
||||
namespace ZShell {
|
||||
|
||||
struct Options {
|
||||
QString outputPath = QStringLiteral("/tmp/align-audit.txt");
|
||||
bool paintedOnly = true; // skip items that draw nothing
|
||||
bool includeInherited =
|
||||
false; // also report items only off because an ancestor is
|
||||
double tolerance = 1e-3; // in device pixels
|
||||
};
|
||||
|
||||
Q_INVOKABLE void runLater(int delayMs, const Options& options = {});
|
||||
|
||||
int run(const Options& options = {});
|
||||
|
||||
} // namespace ZShell
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "alignauditor.hpp"
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
namespace ZShell {
|
||||
|
||||
Options AlignAuditor::options() const {
|
||||
Options o;
|
||||
o.outputPath = m_outputPath;
|
||||
o.paintedOnly = m_paintedOnly;
|
||||
o.includeInherited = m_includeInherited;
|
||||
o.tolerance = m_tolerance;
|
||||
return o;
|
||||
}
|
||||
|
||||
int AlignAuditor::run() {
|
||||
m_lastCount = ZShell::run(options());
|
||||
emit finished(m_lastCount);
|
||||
return m_lastCount;
|
||||
}
|
||||
|
||||
void AlignAuditor::runLater() {
|
||||
QTimer::singleShot(m_delayMs, this, [this] { run(); });
|
||||
}
|
||||
|
||||
} // namespace ZShell
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QtQml/qqmlregistration.h>
|
||||
|
||||
#include "alignaudit.hpp"
|
||||
|
||||
namespace ZShell {
|
||||
|
||||
class AlignAuditor : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(QString outputPath MEMBER m_outputPath NOTIFY outputPathChanged)
|
||||
Q_PROPERTY(int delayMs MEMBER m_delayMs NOTIFY delayMsChanged)
|
||||
Q_PROPERTY(bool paintedOnly MEMBER m_paintedOnly NOTIFY paintedOnlyChanged)
|
||||
Q_PROPERTY(
|
||||
bool includeInherited MEMBER m_includeInherited NOTIFY
|
||||
includeInheritedChanged)
|
||||
Q_PROPERTY(double tolerance MEMBER m_tolerance NOTIFY toleranceChanged)
|
||||
Q_PROPERTY(int lastCount READ lastCount NOTIFY finished)
|
||||
|
||||
public:
|
||||
explicit AlignAuditor(QObject* parent = nullptr) : QObject(parent) {}
|
||||
|
||||
Q_INVOKABLE int run();
|
||||
|
||||
Q_INVOKABLE void runLater();
|
||||
|
||||
int lastCount() const { return m_lastCount; }
|
||||
|
||||
signals:
|
||||
void outputPathChanged();
|
||||
void delayMsChanged();
|
||||
void paintedOnlyChanged();
|
||||
void includeInheritedChanged();
|
||||
void toleranceChanged();
|
||||
void finished(int count);
|
||||
|
||||
private:
|
||||
Options options() const;
|
||||
|
||||
QString m_outputPath = QStringLiteral("/tmp/align-audit.txt");
|
||||
int m_delayMs = 5000;
|
||||
bool m_paintedOnly = true;
|
||||
bool m_includeInherited = false;
|
||||
double m_tolerance = 1e-3;
|
||||
int m_lastCount = 0;
|
||||
};
|
||||
|
||||
} // namespace ZShell
|
||||
@@ -1,8 +1,14 @@
|
||||
#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>
|
||||
@@ -337,4 +343,422 @@ QStringList ZUtils::listFiles(const QString& dir, const QString& suffix) {
|
||||
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
|
||||
|
||||
@@ -53,6 +53,44 @@ class ZUtils : public QObject {
|
||||
Q_INVOKABLE static QList<QQuickItem*> findChildrenMatching(
|
||||
QQuickItem* root, const QString& pattern);
|
||||
|
||||
// Device pixel ratio of the window the item lives in (falls back to the primary screen, then 1.0)
|
||||
Q_INVOKABLE static qreal dprFor(QQuickItem* item);
|
||||
|
||||
// Snap a logical value to the device pixel grid of the item's window.
|
||||
// Centering is detected from the item: anchors.centerIn,
|
||||
// anchors.horizontalCenter / verticalCenter, and Layout alignment. A size
|
||||
// along a centered axis is snapped so the centered edges land on whole
|
||||
// device pixels — using Qt's own rounding rule for that anchor style
|
||||
// (exact halves when `alignWhenCentered` is true, the rounded half of the
|
||||
// anchor target when it is false).
|
||||
Q_INVOKABLE static qreal snap(qreal value, QQuickItem* item);
|
||||
Q_INVOKABLE static qreal snapCeil(qreal value, QQuickItem* item); // text-derived sizes
|
||||
Q_INVOKABLE static qreal snapFloor(qreal value, QQuickItem* item);
|
||||
// Logs where an item sits in device pixels and whether it is aligned.
|
||||
// Returns true if aligned. `tag` is an optional label for the log line.
|
||||
// Items centered on a target are judged by the centering rule above
|
||||
// (whole device size, plus the offset the anchor produces), which is
|
||||
// detected from the item's anchors and Layout alignment.
|
||||
Q_INVOKABLE static bool logPlacement(
|
||||
QQuickItem* item,
|
||||
const QString& tag = QString());
|
||||
|
||||
// Walks a stroked curve path (flat x,y pairs in the item's logical coords,
|
||||
// every point along the curve, not just its defined points) and checks
|
||||
// whether both stroke edges land on device pixel boundaries at `lineWidth`.
|
||||
// Returns true if every sample is aligned.
|
||||
Q_INVOKABLE static bool logCurvePlacement(
|
||||
QQuickItem* item,
|
||||
const QList<qreal>& samples,
|
||||
qreal lineWidth,
|
||||
const QString& tag = QString());
|
||||
|
||||
// Distributes `total` (logical px) across items with the given preferred widths.
|
||||
// Every result is a whole number of device pixels and the results sum exactly to
|
||||
// the snapped total. If the preferred widths don't fit, they shrink proportionally.
|
||||
Q_INVOKABLE static QList<qreal> distribute(
|
||||
qreal total, const QList<qreal>& preferred, QQuickItem* item);
|
||||
|
||||
[[nodiscard]] QString version() const;
|
||||
[[nodiscard]] QString qtVersion() const;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user