performance: Improve the wallet loading time
+ small bug fixes
This commit is contained in:
parent
a6abdbeeef
commit
a55dcf299e
|
@ -26,6 +26,9 @@ QtObject:
|
||||||
extradata: ExtraData
|
extradata: ExtraData
|
||||||
traits: TraitModel
|
traits: TraitModel
|
||||||
ownership: OwnershipModel
|
ownership: OwnershipModel
|
||||||
|
generatedId: string
|
||||||
|
generatedCollectionId: string
|
||||||
|
|
||||||
|
|
||||||
proc setup(self: CollectiblesEntry) =
|
proc setup(self: CollectiblesEntry) =
|
||||||
self.QObject.setup
|
self.QObject.setup
|
||||||
|
@ -45,32 +48,6 @@ QtObject:
|
||||||
self.ownership.setItems(ownership)
|
self.ownership.setItems(ownership)
|
||||||
self.setup()
|
self.setup()
|
||||||
|
|
||||||
proc newCollectibleDetailsFullEntry*(data: backend.Collectible, extradata: ExtraData): CollectiblesEntry =
|
|
||||||
new(result, delete)
|
|
||||||
result.id = data.id
|
|
||||||
result.setData(data)
|
|
||||||
result.extradata = extradata
|
|
||||||
result.setup()
|
|
||||||
|
|
||||||
proc newCollectibleDetailsBasicEntry*(id: backend.CollectibleUniqueID, extradata: ExtraData): CollectiblesEntry =
|
|
||||||
new(result, delete)
|
|
||||||
result.id = id
|
|
||||||
result.extradata = extradata
|
|
||||||
result.traits = newTraitModel()
|
|
||||||
result.ownership = newOwnershipModel()
|
|
||||||
result.setup()
|
|
||||||
|
|
||||||
proc newCollectibleDetailsEmptyEntry*(): CollectiblesEntry =
|
|
||||||
let id = backend.CollectibleUniqueID(
|
|
||||||
contractID: backend.ContractID(
|
|
||||||
chainID: 0,
|
|
||||||
address: ""
|
|
||||||
),
|
|
||||||
tokenID: stint.u256(0)
|
|
||||||
)
|
|
||||||
let extradata = ExtraData()
|
|
||||||
return newCollectibleDetailsBasicEntry(id, extradata)
|
|
||||||
|
|
||||||
proc `$`*(self: CollectiblesEntry): string =
|
proc `$`*(self: CollectiblesEntry): string =
|
||||||
return fmt"""CollectiblesEntry(
|
return fmt"""CollectiblesEntry(
|
||||||
id:{self.id},
|
id:{self.id},
|
||||||
|
@ -78,6 +55,8 @@ QtObject:
|
||||||
extradata:{self.extradata},
|
extradata:{self.extradata},
|
||||||
traits:{self.traits},
|
traits:{self.traits},
|
||||||
ownership:{self.ownership},
|
ownership:{self.ownership},
|
||||||
|
generatedId:{self.generatedId},
|
||||||
|
generatedCollectionId:{self.generatedCollectionId}
|
||||||
)"""
|
)"""
|
||||||
|
|
||||||
proc getCollectibleUniqueID*(self: CollectiblesEntry): backend.CollectibleUniqueID =
|
proc getCollectibleUniqueID*(self: CollectiblesEntry): backend.CollectibleUniqueID =
|
||||||
|
@ -130,10 +109,16 @@ QtObject:
|
||||||
|
|
||||||
# Unique ID to identify collectible, generated by us
|
# Unique ID to identify collectible, generated by us
|
||||||
proc getID*(self: CollectiblesEntry): string =
|
proc getID*(self: CollectiblesEntry): string =
|
||||||
|
return self.generatedId
|
||||||
|
|
||||||
|
proc generateId*(self: CollectiblesEntry): string =
|
||||||
return fmt"{self.getChainId}+{self.getContractAddress}+{self.getTokenID}"
|
return fmt"{self.getChainId}+{self.getContractAddress}+{self.getTokenID}"
|
||||||
|
|
||||||
# Unique ID to identify collection, generated by us
|
# Unique ID to identify collection, generated by us
|
||||||
proc getCollectionID*(self: CollectiblesEntry): string =
|
proc getCollectionID*(self: CollectiblesEntry): string =
|
||||||
|
return self.generatedCollectionId
|
||||||
|
|
||||||
|
proc generateCollectionId*(self: CollectiblesEntry): string =
|
||||||
return fmt"{self.getChainId}+{self.getContractAddress}"
|
return fmt"{self.getChainId}+{self.getContractAddress}"
|
||||||
|
|
||||||
proc nameChanged*(self: CollectiblesEntry) {.signal.}
|
proc nameChanged*(self: CollectiblesEntry) {.signal.}
|
||||||
|
@ -359,4 +344,34 @@ QtObject:
|
||||||
self.communityColorChanged()
|
self.communityColorChanged()
|
||||||
self.communityPrivilegesLevelChanged()
|
self.communityPrivilegesLevelChanged()
|
||||||
self.communityImageChanged()
|
self.communityImageChanged()
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
proc newCollectibleDetailsFullEntry*(data: backend.Collectible, extradata: ExtraData): CollectiblesEntry =
|
||||||
|
new(result, delete)
|
||||||
|
result.id = data.id
|
||||||
|
result.setData(data)
|
||||||
|
result.extradata = extradata
|
||||||
|
result.generatedId = result.generateId()
|
||||||
|
result.generatedCollectionId = result.generateCollectionId()
|
||||||
|
result.setup()
|
||||||
|
|
||||||
|
proc newCollectibleDetailsBasicEntry*(id: backend.CollectibleUniqueID, extradata: ExtraData): CollectiblesEntry =
|
||||||
|
new(result, delete)
|
||||||
|
result.id = id
|
||||||
|
result.extradata = extradata
|
||||||
|
result.traits = newTraitModel()
|
||||||
|
result.ownership = newOwnershipModel()
|
||||||
|
result.generatedId = result.generateId()
|
||||||
|
result.generatedCollectionId = result.generateCollectionId()
|
||||||
|
result.setup()
|
||||||
|
|
||||||
|
proc newCollectibleDetailsEmptyEntry*(): CollectiblesEntry =
|
||||||
|
let id = backend.CollectibleUniqueID(
|
||||||
|
contractID: backend.ContractID(
|
||||||
|
chainID: 0,
|
||||||
|
address: ""
|
||||||
|
),
|
||||||
|
tokenID: stint.u256(0)
|
||||||
|
)
|
||||||
|
let extradata = ExtraData()
|
||||||
|
return newCollectibleDetailsBasicEntry(id, extradata)
|
|
@ -5,6 +5,8 @@
|
||||||
#include <QQmlContext>
|
#include <QQmlContext>
|
||||||
#include <QQmlExpression>
|
#include <QQmlExpression>
|
||||||
#include <QQmlScriptString>
|
#include <QQmlScriptString>
|
||||||
|
#include <QSet>
|
||||||
|
#include <QQmlPropertyMap>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
|
@ -14,9 +16,10 @@ class FastExpressionSorter : public qqsfpm::Sorter
|
||||||
Q_PROPERTY(QQmlScriptString expression READ expression
|
Q_PROPERTY(QQmlScriptString expression READ expression
|
||||||
WRITE setExpression NOTIFY expressionChanged)
|
WRITE setExpression NOTIFY expressionChanged)
|
||||||
|
|
||||||
Q_PROPERTY(QStringList expectedRoles READ expectedRoles
|
Q_PROPERTY(QSet<QByteArray> expectedRoles READ expectedRoles
|
||||||
WRITE setExpectedRoles NOTIFY expectedRolesChanged)
|
WRITE setExpectedRoles NOTIFY expectedRolesChanged)
|
||||||
public:
|
public:
|
||||||
|
|
||||||
using qqsfpm::Sorter::Sorter;
|
using qqsfpm::Sorter::Sorter;
|
||||||
|
|
||||||
const QQmlScriptString& expression() const;
|
const QQmlScriptString& expression() const;
|
||||||
|
@ -24,16 +27,18 @@ public:
|
||||||
|
|
||||||
void proxyModelCompleted(const qqsfpm::QQmlSortFilterProxyModel& proxyModel) override;
|
void proxyModelCompleted(const qqsfpm::QQmlSortFilterProxyModel& proxyModel) override;
|
||||||
|
|
||||||
void setExpectedRoles(const QStringList& expectedRoles);
|
void setExpectedRoles(const QSet<QByteArray>& expectedRoles);
|
||||||
const QStringList& expectedRoles() const;
|
const QSet<QByteArray>& expectedRoles() const;
|
||||||
|
|
||||||
|
void queueInvalidate();
|
||||||
|
void onInvalidate();
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void expressionChanged();
|
void expressionChanged();
|
||||||
void expectedRolesChanged();
|
void expectedRolesChanged();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
int compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight,
|
int compare(const QModelIndex& sourceLeft, const QModelIndex& sourceRight, const qqsfpm::QQmlSortFilterProxyModel& proxyModel) const override;
|
||||||
const qqsfpm::QQmlSortFilterProxyModel& proxyModel) const override;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updateContext(const qqsfpm::QQmlSortFilterProxyModel& proxyModel);
|
void updateContext(const qqsfpm::QQmlSortFilterProxyModel& proxyModel);
|
||||||
|
@ -44,5 +49,10 @@ private:
|
||||||
std::unique_ptr<QQmlExpression> m_expression;
|
std::unique_ptr<QQmlExpression> m_expression;
|
||||||
std::unique_ptr<QQmlContext> m_context;
|
std::unique_ptr<QQmlContext> m_context;
|
||||||
|
|
||||||
QStringList m_expectedRoles;
|
QSet<QByteArray> m_expectedRoles;
|
||||||
|
|
||||||
|
bool m_queuedInvalidate { false };
|
||||||
|
|
||||||
|
mutable QQmlPropertyMap m_modelLeftMap;
|
||||||
|
mutable QQmlPropertyMap m_modelRightMap;
|
||||||
};
|
};
|
||||||
|
|
|
@ -50,12 +50,28 @@ void FastExpressionSorter::setExpression(const QQmlScriptString& scriptString)
|
||||||
updateExpression();
|
updateExpression();
|
||||||
|
|
||||||
emit expressionChanged();
|
emit expressionChanged();
|
||||||
invalidate();
|
queueInvalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FastExpressionSorter::queueInvalidate()
|
||||||
|
{
|
||||||
|
if (m_queuedInvalidate)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_queuedInvalidate = true;
|
||||||
|
QMetaObject::invokeMethod(this, &FastExpressionSorter::invalidate, Qt::QueuedConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FastExpressionSorter::onInvalidate()
|
||||||
|
{
|
||||||
|
m_queuedInvalidate = false;
|
||||||
|
Sorter::invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
void FastExpressionSorter::proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel)
|
void FastExpressionSorter::proxyModelCompleted(const QQmlSortFilterProxyModel& proxyModel)
|
||||||
{
|
{
|
||||||
updateContext(proxyModel);
|
updateContext(proxyModel);
|
||||||
|
Sorter::proxyModelCompleted(proxyModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
@ -63,7 +79,7 @@ void FastExpressionSorter::proxyModelCompleted(const QQmlSortFilterProxyModel& p
|
||||||
|
|
||||||
List of role names intended to be available in the expression's context.
|
List of role names intended to be available in the expression's context.
|
||||||
*/
|
*/
|
||||||
void FastExpressionSorter::setExpectedRoles(const QStringList& expectedRoles)
|
void FastExpressionSorter::setExpectedRoles(const QSet<QByteArray>& expectedRoles)
|
||||||
{
|
{
|
||||||
if (m_expectedRoles == expectedRoles)
|
if (m_expectedRoles == expectedRoles)
|
||||||
return;
|
return;
|
||||||
|
@ -71,44 +87,45 @@ void FastExpressionSorter::setExpectedRoles(const QStringList& expectedRoles)
|
||||||
m_expectedRoles = expectedRoles;
|
m_expectedRoles = expectedRoles;
|
||||||
emit expectedRolesChanged();
|
emit expectedRolesChanged();
|
||||||
|
|
||||||
invalidate();
|
queueInvalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
const QStringList &FastExpressionSorter::expectedRoles() const
|
const QSet<QByteArray> &FastExpressionSorter::expectedRoles() const
|
||||||
{
|
{
|
||||||
return m_expectedRoles;
|
return m_expectedRoles;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool evaluateBoolExpression(QQmlExpression& expression)
|
int evaluateIntExpression(QQmlExpression& expression)
|
||||||
{
|
{
|
||||||
QVariant variantResult = expression.evaluate();
|
QVariant variantResult = expression.evaluate();
|
||||||
if (expression.hasError()) {
|
if (expression.hasError()) {
|
||||||
qWarning() << expression.error();
|
qWarning() << expression.error();
|
||||||
return false;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (variantResult.canConvert<bool>())
|
if (variantResult.canConvert<int>())
|
||||||
return variantResult.toBool();
|
return variantResult.toInt();
|
||||||
|
|
||||||
qWarning("%s:%i:%i : Can't convert result to bool",
|
qWarning("%s:%i:%i : Can't convert result to int",
|
||||||
expression.sourceFile().toUtf8().data(),
|
expression.sourceFile().toUtf8().data(),
|
||||||
expression.lineNumber(),
|
expression.lineNumber(),
|
||||||
expression.columnNumber());
|
expression.columnNumber());
|
||||||
return false;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int FastExpressionSorter::compare(const QModelIndex& sourceLeft,
|
int FastExpressionSorter::compare(const QModelIndex& sourceLeft,
|
||||||
const QModelIndex& sourceRight,
|
const QModelIndex& sourceRight,
|
||||||
const QQmlSortFilterProxyModel& proxyModel) const
|
const QQmlSortFilterProxyModel& proxyModel) const
|
||||||
{
|
{
|
||||||
if (m_scriptString.isEmpty())
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
QVariantMap modelLeftMap, modelRightMap;
|
if (m_scriptString.isEmpty() || !m_context || !m_expression)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
m_expression->setNotifyOnValueChanged(false);
|
||||||
|
|
||||||
QHash<int, QByteArray> roles = proxyModel.roleNames();
|
QHash<int, QByteArray> roles = proxyModel.roleNames();
|
||||||
|
|
||||||
QQmlContext context(qmlContext(this));
|
|
||||||
|
|
||||||
for (auto it = roles.cbegin(); it != roles.cend(); ++it) {
|
for (auto it = roles.cbegin(); it != roles.cend(); ++it) {
|
||||||
auto role = it.key();
|
auto role = it.key();
|
||||||
auto name = it.value();
|
auto name = it.value();
|
||||||
|
@ -116,50 +133,42 @@ int FastExpressionSorter::compare(const QModelIndex& sourceLeft,
|
||||||
if (!m_expectedRoles.contains(name))
|
if (!m_expectedRoles.contains(name))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
modelLeftMap.insert(name, proxyModel.sourceData(sourceLeft, role));
|
m_modelLeftMap.insert(name, proxyModel.sourceData(sourceLeft, role));
|
||||||
modelRightMap.insert(name, proxyModel.sourceData(sourceRight, role));
|
m_modelRightMap.insert(name, proxyModel.sourceData(sourceRight, role));
|
||||||
}
|
}
|
||||||
modelLeftMap.insert(QStringLiteral("index"), sourceLeft.row());
|
m_modelLeftMap.insert(QStringLiteral("index"), sourceLeft.row());
|
||||||
modelRightMap.insert(QStringLiteral("index"), sourceRight.row());
|
m_modelRightMap.insert(QStringLiteral("index"), sourceRight.row());
|
||||||
|
|
||||||
QQmlExpression expression(m_scriptString, &context);
|
m_expression->setNotifyOnValueChanged(true);
|
||||||
|
|
||||||
context.setContextProperty(QStringLiteral("modelLeft"), modelLeftMap);
|
return evaluateIntExpression(*m_expression);
|
||||||
context.setContextProperty(QStringLiteral("modelRight"), modelRightMap);
|
|
||||||
|
|
||||||
if (evaluateBoolExpression(expression))
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
context.setContextProperty(QStringLiteral("modelLeft"), modelRightMap);
|
|
||||||
context.setContextProperty(QStringLiteral("modelRight"), modelLeftMap);
|
|
||||||
|
|
||||||
if (evaluateBoolExpression(expression))
|
|
||||||
return 1;
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FastExpressionSorter::updateContext(const QQmlSortFilterProxyModel& proxyModel)
|
void FastExpressionSorter::updateContext(const QQmlSortFilterProxyModel& proxyModel)
|
||||||
{
|
{
|
||||||
m_context = std::make_unique<QQmlContext>(qmlContext(this));
|
m_context = std::make_unique<QQmlContext>(qmlContext(this));
|
||||||
|
updateExpression();
|
||||||
|
|
||||||
QVariantMap modelLeftMap, modelRightMap;
|
if (!m_expression)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_expression->setNotifyOnValueChanged(false);
|
||||||
|
|
||||||
const auto roleNames = proxyModel.roleNames();
|
const auto roleNames = proxyModel.roleNames();
|
||||||
for (const QByteArray& name : roleNames) {
|
for (const QByteArray& name : roleNames) {
|
||||||
if (!m_expectedRoles.contains(name))
|
if (!m_expectedRoles.contains(name))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
modelLeftMap.insert(name, {});
|
m_modelLeftMap.insert(name, {});
|
||||||
modelRightMap.insert(name, {});
|
m_modelRightMap.insert(name, {});
|
||||||
}
|
}
|
||||||
modelLeftMap.insert(QStringLiteral("index"), -1);
|
m_modelLeftMap.insert(QStringLiteral("index"), -1);
|
||||||
modelRightMap.insert(QStringLiteral("index"), -1);
|
m_modelRightMap.insert(QStringLiteral("index"), -1);
|
||||||
|
|
||||||
m_context->setContextProperty(QStringLiteral("modelLeft"), modelLeftMap);
|
m_context->setContextProperty(QStringLiteral("modelLeft"), &m_modelLeftMap);
|
||||||
m_context->setContextProperty(QStringLiteral("modelRight"), modelRightMap);
|
m_context->setContextProperty(QStringLiteral("modelRight"), &m_modelRightMap);
|
||||||
|
|
||||||
updateExpression();
|
m_expression->setNotifyOnValueChanged(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void FastExpressionSorter::updateExpression()
|
void FastExpressionSorter::updateExpression()
|
||||||
|
@ -171,7 +180,7 @@ void FastExpressionSorter::updateExpression()
|
||||||
m_context.get());
|
m_context.get());
|
||||||
|
|
||||||
connect(m_expression.get(), &QQmlExpression::valueChanged, this,
|
connect(m_expression.get(), &QQmlExpression::valueChanged, this,
|
||||||
&FastExpressionSorter::invalidate);
|
&FastExpressionSorter::queueInvalidate);
|
||||||
m_expression->setNotifyOnValueChanged(true);
|
m_expression->setNotifyOnValueChanged(true);
|
||||||
m_expression->evaluate();
|
m_expression->evaluate();
|
||||||
}
|
}
|
||||||
|
|
|
@ -239,8 +239,7 @@ void ManageTokensController::loadSettings()
|
||||||
{
|
{
|
||||||
Q_ASSERT(!m_settingsKey.isEmpty());
|
Q_ASSERT(!m_settingsKey.isEmpty());
|
||||||
|
|
||||||
setSettingsDirty(true);
|
SerializedTokenData result;
|
||||||
m_settingsData.clear();
|
|
||||||
|
|
||||||
// load from QSettings
|
// load from QSettings
|
||||||
m_settings.beginGroup(settingsGroupName());
|
m_settings.beginGroup(settingsGroupName());
|
||||||
|
@ -256,7 +255,7 @@ void ManageTokensController::loadSettings()
|
||||||
const auto pos = m_settings.value(QStringLiteral("pos"), INT_MAX).toInt();
|
const auto pos = m_settings.value(QStringLiteral("pos"), INT_MAX).toInt();
|
||||||
const auto visible = m_settings.value(QStringLiteral("visible"), true).toBool();
|
const auto visible = m_settings.value(QStringLiteral("visible"), true).toBool();
|
||||||
const auto groupId = m_settings.value(QStringLiteral("groupId")).toString();
|
const auto groupId = m_settings.value(QStringLiteral("groupId")).toString();
|
||||||
m_settingsData.insert(symbol, {pos, visible, groupId});
|
result.insert(symbol, {pos, visible, groupId});
|
||||||
}
|
}
|
||||||
m_settings.endArray();
|
m_settings.endArray();
|
||||||
|
|
||||||
|
@ -277,7 +276,11 @@ void ManageTokensController::loadSettings()
|
||||||
setArrangeByCollection(m_settings.value(QStringLiteral("ArrangeByCollection"), false).toBool());
|
setArrangeByCollection(m_settings.value(QStringLiteral("ArrangeByCollection"), false).toBool());
|
||||||
|
|
||||||
m_settings.endGroup();
|
m_settings.endGroup();
|
||||||
setSettingsDirty(false);
|
|
||||||
|
if (result != m_settingsData) {
|
||||||
|
m_settingsData = result;
|
||||||
|
setSettingsDirty(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ManageTokensController::setSettingsDirty(bool dirty)
|
void ManageTokensController::setSettingsDirty(bool dirty)
|
||||||
|
@ -353,7 +356,7 @@ void ManageTokensController::settingsHideGroupTokens(const QString& groupId, con
|
||||||
saveSettings(true);
|
saveSettings(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ManageTokensController::lessThan(const QString& lhsSymbol, const QString& rhsSymbol) const
|
int ManageTokensController::compareTokens(const QString& lhsSymbol, const QString& rhsSymbol) const
|
||||||
{
|
{
|
||||||
int leftPos, rightPos;
|
int leftPos, rightPos;
|
||||||
bool leftVisible, rightVisible;
|
bool leftVisible, rightVisible;
|
||||||
|
@ -365,7 +368,11 @@ bool ManageTokensController::lessThan(const QString& lhsSymbol, const QString& r
|
||||||
leftPos = leftVisible ? leftPos : INT_MAX;
|
leftPos = leftVisible ? leftPos : INT_MAX;
|
||||||
rightPos = rightVisible ? rightPos : INT_MAX;
|
rightPos = rightVisible ? rightPos : INT_MAX;
|
||||||
|
|
||||||
return leftPos <= rightPos;
|
if (leftPos < rightPos)
|
||||||
|
return -1;
|
||||||
|
if (leftPos > rightPos)
|
||||||
|
return 1;
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ManageTokensController::filterAcceptsSymbol(const QString& symbol) const
|
bool ManageTokensController::filterAcceptsSymbol(const QString& symbol) const
|
||||||
|
|
|
@ -52,7 +52,7 @@ public:
|
||||||
Q_INVOKABLE void settingsHideToken(const QString& symbol);
|
Q_INVOKABLE void settingsHideToken(const QString& symbol);
|
||||||
Q_INVOKABLE void settingsHideGroupTokens(const QString& groupId, const QStringList& symbols);
|
Q_INVOKABLE void settingsHideGroupTokens(const QString& groupId, const QStringList& symbols);
|
||||||
|
|
||||||
Q_INVOKABLE bool lessThan(const QString& lhsSymbol, const QString& rhsSymbol) const;
|
Q_INVOKABLE int compareTokens(const QString& lhsSymbol, const QString& rhsSymbol) const;
|
||||||
Q_INVOKABLE bool filterAcceptsSymbol(const QString& symbol) const;
|
Q_INVOKABLE bool filterAcceptsSymbol(const QString& symbol) const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
|
@ -17,17 +17,21 @@ Item {
|
||||||
property int d: 1
|
property int d: 1
|
||||||
|
|
||||||
property alias sorterEnabled: sorter.enabled
|
property alias sorterEnabled: sorter.enabled
|
||||||
|
property alias sortingAscending: sorter.ascendingOrder
|
||||||
|
property alias sorters: testModel.sorters
|
||||||
|
|
||||||
readonly property ListModel source: ListModel {
|
readonly property ListModel source: ListModel {
|
||||||
id: listModel
|
id: listModel
|
||||||
|
|
||||||
ListElement { a: 1; b: 11; c: 101 }
|
ListElement { a: 1; b: 11; c: 100 }
|
||||||
ListElement { a: 2; b: 12; c: 102 }
|
ListElement { a: 2; b: 11; c: 101 }
|
||||||
ListElement { a: 3; b: 13; c: 103 }
|
ListElement { a: 3; b: 13; c: 103 }
|
||||||
ListElement { a: 4; b: 14; c: 104 }
|
ListElement { a: 4; b: 14; c: 104 }
|
||||||
ListElement { a: 5; b: 15; c: 105 }
|
ListElement { a: 5; b: 15; c: 105 }
|
||||||
ListElement { a: 6; b: 16; c: 106 }
|
ListElement { a: 6; b: 16; c: 106 }
|
||||||
|
ListElement { a: 2; b: 12; c: 101 }
|
||||||
ListElement { a: 7; b: 17; c: 107 }
|
ListElement { a: 7; b: 17; c: 107 }
|
||||||
|
ListElement { a: 7; b: 17; c: 108 }
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property ModelAccessObserverProxy observer: ModelAccessObserverProxy {
|
readonly property ModelAccessObserverProxy observer: ModelAccessObserverProxy {
|
||||||
|
@ -44,28 +48,71 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property SortFilterProxyModel model: SortFilterProxyModel {
|
property SortFilterProxyModel model: SortFilterProxyModel {
|
||||||
id: testModel
|
id: testModel
|
||||||
|
|
||||||
sourceModel: observerProxy
|
sourceModel: observerProxy
|
||||||
|
|
||||||
sorters: FastExpressionSorter {
|
sorters: [sorter]
|
||||||
id: sorter
|
}
|
||||||
|
|
||||||
expression: {
|
readonly property Component modelWithPriorityComponent: Component {
|
||||||
return d ? modelLeft.a < modelRight.a
|
SortFilterProxyModel {
|
||||||
: modelLeft.a > modelRight.a
|
id: testModelWithPriority
|
||||||
}
|
|
||||||
|
|
||||||
expectedRoles: ["a"]
|
sourceModel: observerProxy
|
||||||
|
|
||||||
|
sorters: [sorter, otherSorter, roleSorter]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property FastExpressionSorter sorter: FastExpressionSorter {
|
||||||
|
id: sorter
|
||||||
|
|
||||||
|
expression: {
|
||||||
|
if (modelLeft.a < modelRight.a)
|
||||||
|
return d ? -1 : 1
|
||||||
|
else if (modelLeft.a > modelRight.a)
|
||||||
|
return d ? 1 : -1
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedRoles: ["a"]
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property FastExpressionSorter otherSorter: FastExpressionSorter {
|
||||||
|
id: otherSorter
|
||||||
|
|
||||||
|
expression: {
|
||||||
|
if (modelLeft.b > modelRight.b)
|
||||||
|
return -1
|
||||||
|
else if (modelLeft.b < modelRight.b)
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedRoles: ["b"]
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property RoleSorter roleSorter: RoleSorter {
|
||||||
|
id: roleSorter
|
||||||
|
|
||||||
|
roleName: "c"
|
||||||
|
ascendingOrder: false
|
||||||
|
}
|
||||||
|
|
||||||
readonly property SignalSpy rowsRemovedSpy: SignalSpy {
|
readonly property SignalSpy rowsRemovedSpy: SignalSpy {
|
||||||
target: testModel
|
target: testModel
|
||||||
signalName: "rowsRemoved"
|
signalName: "rowsRemoved"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property SignalSpy layoutChangedSpy: SignalSpy {
|
||||||
|
id: layoutChangedSpy
|
||||||
|
target: testModel
|
||||||
|
signalName: "layoutChanged"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,17 +123,17 @@ Item {
|
||||||
const obj = createTemporaryObject(testComponent, root)
|
const obj = createTemporaryObject(testComponent, root)
|
||||||
const count = obj.model.count
|
const count = obj.model.count
|
||||||
|
|
||||||
compare(count, 7)
|
compare(count, 9)
|
||||||
verify(obj.observer.accessCounter
|
verify(obj.observer.accessCounter
|
||||||
< count * Math.ceil(Math.log2(count)) * 3)
|
< count * Math.ceil(Math.log2(count)) * 3)
|
||||||
compare(obj.observer.accessedRoles.size, 1)
|
compare(obj.observer.accessedRoles.size, 1)
|
||||||
|
|
||||||
compare(obj.model.get(0).a, 1)
|
compare(obj.model.get(0).a, 1)
|
||||||
compare(obj.model.get(1).a, 2)
|
compare(obj.model.get(1).a, 2)
|
||||||
compare(obj.model.get(6).a, 7)
|
compare(obj.model.get(7).a, 7)
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_filteringAfterContextChange() {
|
function test_sortingAfterContextChange() {
|
||||||
const obj = createTemporaryObject(testComponent, root)
|
const obj = createTemporaryObject(testComponent, root)
|
||||||
const count = obj.model.count
|
const count = obj.model.count
|
||||||
|
|
||||||
|
@ -98,9 +145,9 @@ Item {
|
||||||
< count * Math.ceil(Math.log2(count)) * 3)
|
< count * Math.ceil(Math.log2(count)) * 3)
|
||||||
compare(obj.observer.accessedRoles.size, 1)
|
compare(obj.observer.accessedRoles.size, 1)
|
||||||
|
|
||||||
compare(obj.model.get(0).a, 7)
|
tryVerify(() => obj.model.get(0).a, 7)
|
||||||
compare(obj.model.get(1).a, 6)
|
tryVerify(() => obj.model.get(1).a, 6)
|
||||||
compare(obj.model.get(6).a, 1)
|
tryVerify(() => obj.model.get(6).a, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
function test_enabled() {
|
function test_enabled() {
|
||||||
|
@ -110,7 +157,7 @@ Item {
|
||||||
|
|
||||||
compare(obj.model.get(0).a, 1)
|
compare(obj.model.get(0).a, 1)
|
||||||
compare(obj.model.get(1).a, 2)
|
compare(obj.model.get(1).a, 2)
|
||||||
compare(obj.model.get(6).a, 7)
|
compare(obj.model.get(7).a, 7)
|
||||||
|
|
||||||
obj.observer.accessedRoles.clear()
|
obj.observer.accessedRoles.clear()
|
||||||
obj.observer.accessCounter = 0
|
obj.observer.accessCounter = 0
|
||||||
|
@ -123,8 +170,236 @@ Item {
|
||||||
compare(obj.observer.accessedRoles.size, 1)
|
compare(obj.observer.accessedRoles.size, 1)
|
||||||
|
|
||||||
compare(obj.model.get(0).a, 7)
|
compare(obj.model.get(0).a, 7)
|
||||||
compare(obj.model.get(1).a, 6)
|
compare(obj.model.get(1).a, 7)
|
||||||
compare(obj.model.get(6).a, 1)
|
compare(obj.model.get(7).a, 2)
|
||||||
|
compare(obj.model.get(8).a, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_sortingDescending() {
|
||||||
|
const obj = createTemporaryObject(testComponent, root)
|
||||||
|
|
||||||
|
const count = obj.model.count
|
||||||
|
|
||||||
|
verify(obj.observer.accessCounter
|
||||||
|
< count * Math.ceil(Math.log2(count)) * 3)
|
||||||
|
compare(obj.observer.accessedRoles.size, 1)
|
||||||
|
|
||||||
|
|
||||||
|
compare(obj.model.get(0).a, 1)
|
||||||
|
compare(obj.model.get(1).a, 2)
|
||||||
|
compare(obj.model.get(7).a, 7)
|
||||||
|
|
||||||
|
obj.observer.accessCounter = 0
|
||||||
|
|
||||||
|
obj.sortingAscending = false
|
||||||
|
|
||||||
|
tryVerify(() => obj.observer.accessCounter
|
||||||
|
< count * Math.ceil(Math.log2(count)) * 3)
|
||||||
|
|
||||||
|
tryVerify(() => obj.model.get(0).a, 7)
|
||||||
|
tryVerify(() => obj.model.get(1).a, 6)
|
||||||
|
tryVerify(() => obj.model.get(7).a, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_sortingDescendingAfterEnablingSorting() {
|
||||||
|
const obj = createTemporaryObject(testComponent, root, { sorterEnabled: false, sortingAscending: false })
|
||||||
|
|
||||||
|
compare(obj.observer.accessCounter, 0)
|
||||||
|
compare(obj.observer.accessedRoles.size, 0)
|
||||||
|
|
||||||
|
compare(obj.model.get(0).a, 1)
|
||||||
|
compare(obj.model.get(1).a, 2)
|
||||||
|
compare(obj.model.get(7).a, 7)
|
||||||
|
|
||||||
|
obj.observer.accessedRoles.clear()
|
||||||
|
obj.observer.accessCounter = 0
|
||||||
|
|
||||||
|
obj.sorterEnabled = true
|
||||||
|
|
||||||
|
const count = obj.model.count
|
||||||
|
|
||||||
|
verify(obj.observer.accessCounter
|
||||||
|
< count * Math.ceil(Math.log2(count)) * 3)
|
||||||
|
|
||||||
|
compare(obj.observer.accessedRoles.size, 1)
|
||||||
|
|
||||||
|
compare(obj.model.get(0).a, 7)
|
||||||
|
compare(obj.model.get(1).a, 7)
|
||||||
|
compare(obj.model.get(8).a, 1)
|
||||||
|
|
||||||
|
obj.observer.accessedRoles.clear()
|
||||||
|
obj.observer.accessCounter = 0
|
||||||
|
|
||||||
|
obj.sorterEnabled = false
|
||||||
|
|
||||||
|
verify(obj.observer.accessCounter == 0)
|
||||||
|
|
||||||
|
compare(obj.observer.accessedRoles.size, 0)
|
||||||
|
|
||||||
|
compare(obj.model.get(0).a, 1)
|
||||||
|
compare(obj.model.get(1).a, 2)
|
||||||
|
compare(obj.model.get(7).a, 7)
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_stableSorting() {
|
||||||
|
const obj = createTemporaryObject(testComponent, root)
|
||||||
|
|
||||||
|
compare(obj.model.get(0).a, 1)
|
||||||
|
compare(obj.model.get(1).a, 2)
|
||||||
|
compare(obj.model.get(2).a, 2)
|
||||||
|
compare(obj.model.get(0).b, 11)
|
||||||
|
compare(obj.model.get(1).b, 11)
|
||||||
|
compare(obj.model.get(2).b, 12)
|
||||||
|
compare(obj.model.get(0).c, 100)
|
||||||
|
compare(obj.model.get(1).c, 101)
|
||||||
|
compare(obj.model.get(2).c, 101)
|
||||||
|
|
||||||
|
obj.sortingAscending = false
|
||||||
|
|
||||||
|
compare(obj.model.get(8).a, 1)
|
||||||
|
compare(obj.model.get(7).a, 2)
|
||||||
|
compare(obj.model.get(6).a, 2)
|
||||||
|
compare(obj.model.get(8).b, 11)
|
||||||
|
compare(obj.model.get(7).b, 12)
|
||||||
|
compare(obj.model.get(6).b, 11)
|
||||||
|
compare(obj.model.get(8).c, 100)
|
||||||
|
compare(obj.model.get(7).c, 101)
|
||||||
|
compare(obj.model.get(6).c, 101)
|
||||||
|
|
||||||
|
|
||||||
|
obj.sortingAscending = true
|
||||||
|
|
||||||
|
compare(obj.model.get(0).a, 1)
|
||||||
|
compare(obj.model.get(1).a, 2)
|
||||||
|
compare(obj.model.get(2).a, 2)
|
||||||
|
compare(obj.model.get(0).b, 11)
|
||||||
|
compare(obj.model.get(1).b, 11)
|
||||||
|
compare(obj.model.get(2).b, 12)
|
||||||
|
compare(obj.model.get(0).c, 100)
|
||||||
|
compare(obj.model.get(1).c, 101)
|
||||||
|
compare(obj.model.get(2).c, 101)
|
||||||
|
|
||||||
|
obj.source.append({a: 2, b: 13, c: 101})
|
||||||
|
|
||||||
|
compare(obj.model.get(0).a, 1)
|
||||||
|
compare(obj.model.get(1).a, 2)
|
||||||
|
compare(obj.model.get(2).a, 2)
|
||||||
|
compare(obj.model.get(3).a, 2)
|
||||||
|
compare(obj.model.get(0).b, 11)
|
||||||
|
compare(obj.model.get(1).b, 11)
|
||||||
|
compare(obj.model.get(2).b, 12)
|
||||||
|
compare(obj.model.get(3).b, 13)
|
||||||
|
compare(obj.model.get(0).c, 100)
|
||||||
|
compare(obj.model.get(1).c, 101)
|
||||||
|
compare(obj.model.get(2).c, 101)
|
||||||
|
compare(obj.model.get(3).c, 101)
|
||||||
|
|
||||||
|
obj.sortingAscending = false
|
||||||
|
|
||||||
|
compare(obj.model.get(9).a, 1)
|
||||||
|
compare(obj.model.get(8).a, 2)
|
||||||
|
compare(obj.model.get(7).a, 2)
|
||||||
|
compare(obj.model.get(6).a, 2)
|
||||||
|
compare(obj.model.get(9).b, 11)
|
||||||
|
compare(obj.model.get(8).b, 13)
|
||||||
|
compare(obj.model.get(7).b, 12)
|
||||||
|
compare(obj.model.get(6).b, 11)
|
||||||
|
compare(obj.model.get(9).c, 100)
|
||||||
|
compare(obj.model.get(8).c, 101)
|
||||||
|
compare(obj.model.get(7).c, 101)
|
||||||
|
compare(obj.model.get(6).c, 101)
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_default_stableSorting() {
|
||||||
|
const obj = createTemporaryObject(testComponent, root, { sorters: [] })
|
||||||
|
|
||||||
|
obj.model.sortRoleName = "a"
|
||||||
|
obj.model.ascendingSortOrder = true
|
||||||
|
|
||||||
|
compare(obj.model.get(0).a, 1)
|
||||||
|
compare(obj.model.get(1).a, 2)
|
||||||
|
compare(obj.model.get(2).a, 2)
|
||||||
|
compare(obj.model.get(0).b, 11)
|
||||||
|
compare(obj.model.get(1).b, 11)
|
||||||
|
compare(obj.model.get(2).b, 12)
|
||||||
|
compare(obj.model.get(0).c, 100)
|
||||||
|
compare(obj.model.get(1).c, 101)
|
||||||
|
compare(obj.model.get(2).c, 101)
|
||||||
|
|
||||||
|
obj.model.ascendingSortOrder = false
|
||||||
|
|
||||||
|
compare(obj.model.get(8).a, 1)
|
||||||
|
compare(obj.model.get(7).a, 2)
|
||||||
|
compare(obj.model.get(6).a, 2)
|
||||||
|
compare(obj.model.get(8).b, 11)
|
||||||
|
compare(obj.model.get(7).b, 12)
|
||||||
|
compare(obj.model.get(6).b, 11)
|
||||||
|
compare(obj.model.get(8).c, 100)
|
||||||
|
compare(obj.model.get(7).c, 101)
|
||||||
|
compare(obj.model.get(6).c, 101)
|
||||||
|
|
||||||
|
|
||||||
|
obj.model.ascendingSortOrder = true
|
||||||
|
|
||||||
|
compare(obj.model.get(0).a, 1)
|
||||||
|
compare(obj.model.get(1).a, 2)
|
||||||
|
compare(obj.model.get(2).a, 2)
|
||||||
|
compare(obj.model.get(0).b, 11)
|
||||||
|
compare(obj.model.get(1).b, 11)
|
||||||
|
compare(obj.model.get(2).b, 12)
|
||||||
|
compare(obj.model.get(0).c, 100)
|
||||||
|
compare(obj.model.get(1).c, 101)
|
||||||
|
compare(obj.model.get(2).c, 101)
|
||||||
|
|
||||||
|
obj.source.append({a: 2, b: 13, c: 101})
|
||||||
|
|
||||||
|
compare(obj.model.get(0).a, 1)
|
||||||
|
compare(obj.model.get(1).a, 2)
|
||||||
|
compare(obj.model.get(2).a, 2)
|
||||||
|
compare(obj.model.get(3).a, 2)
|
||||||
|
compare(obj.model.get(0).b, 11)
|
||||||
|
compare(obj.model.get(1).b, 11)
|
||||||
|
compare(obj.model.get(2).b, 12)
|
||||||
|
compare(obj.model.get(3).b, 13)
|
||||||
|
compare(obj.model.get(0).c, 100)
|
||||||
|
compare(obj.model.get(1).c, 101)
|
||||||
|
compare(obj.model.get(2).c, 101)
|
||||||
|
compare(obj.model.get(3).c, 101)
|
||||||
|
|
||||||
|
obj.model.ascendingSortOrder = false
|
||||||
|
|
||||||
|
compare(obj.model.get(9).a, 1)
|
||||||
|
compare(obj.model.get(8).a, 2)
|
||||||
|
compare(obj.model.get(7).a, 2)
|
||||||
|
compare(obj.model.get(6).a, 2)
|
||||||
|
compare(obj.model.get(9).b, 11)
|
||||||
|
compare(obj.model.get(8).b, 13)
|
||||||
|
compare(obj.model.get(7).b, 12)
|
||||||
|
compare(obj.model.get(6).b, 11)
|
||||||
|
compare(obj.model.get(9).c, 100)
|
||||||
|
compare(obj.model.get(8).c, 101)
|
||||||
|
compare(obj.model.get(7).c, 101)
|
||||||
|
compare(obj.model.get(6).c, 101)
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_sortWithPriority() {
|
||||||
|
const obj = createTemporaryObject(testComponent, root)
|
||||||
|
|
||||||
|
obj.model = createTemporaryObject(obj.modelWithPriorityComponent, obj)
|
||||||
|
|
||||||
|
compare(obj.model.get(0).a, 1)
|
||||||
|
compare(obj.model.get(1).a, 2)
|
||||||
|
compare(obj.model.get(2).a, 2)
|
||||||
|
compare(obj.model.get(0).b, 11)
|
||||||
|
compare(obj.model.get(1).b, 12) // descending "b"
|
||||||
|
compare(obj.model.get(2).b, 11)
|
||||||
|
compare(obj.model.get(0).c, 100)
|
||||||
|
compare(obj.model.get(1).c, 101)
|
||||||
|
compare(obj.model.get(2).c, 101)
|
||||||
|
compare(obj.model.get(7).a, 7)
|
||||||
|
compare(obj.model.get(8).a, 7)
|
||||||
|
compare(obj.model.get(7).c, 108) // descending "c"
|
||||||
|
compare(obj.model.get(8).c, 107)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,7 +102,12 @@ StatusListView {
|
||||||
}
|
}
|
||||||
sorters: FastExpressionSorter {
|
sorters: FastExpressionSorter {
|
||||||
expression: {
|
expression: {
|
||||||
return modelLeft.enabledNetworkBalance > modelRight.enabledNetworkBalance // descending, biggest first
|
if (modelLeft.enabledNetworkBalance > modelRight.enabledNetworkBalance)
|
||||||
|
return -1 // descending, biggest first
|
||||||
|
else if (modelLeft.enabledNetworkBalance < modelRight.enabledNetworkBalance)
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
expectedRoles: ["enabledNetworkBalance"]
|
expectedRoles: ["enabledNetworkBalance"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -133,7 +133,7 @@ ColumnLayout {
|
||||||
FastExpressionSorter {
|
FastExpressionSorter {
|
||||||
expression: {
|
expression: {
|
||||||
d.controller.settingsDirty
|
d.controller.settingsDirty
|
||||||
return d.controller.lessThan(modelLeft.symbol, modelRight.symbol)
|
return d.controller.compareTokens(modelLeft.symbol, modelRight.symbol)
|
||||||
}
|
}
|
||||||
enabled: d.isCustomView
|
enabled: d.isCustomView
|
||||||
expectedRoles: ["symbol"]
|
expectedRoles: ["symbol"]
|
||||||
|
|
|
@ -131,7 +131,6 @@ RightTabBaseView {
|
||||||
case 2: return historyView
|
case 2: return historyView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
active: visible
|
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: assetsView
|
id: assetsView
|
||||||
|
|
|
@ -140,7 +140,7 @@ ColumnLayout {
|
||||||
FastExpressionSorter {
|
FastExpressionSorter {
|
||||||
expression: {
|
expression: {
|
||||||
d.controller.settingsDirty
|
d.controller.settingsDirty
|
||||||
return d.controller.lessThan(modelLeft.symbol, modelRight.symbol)
|
return d.controller.compareTokens(modelLeft.symbol, modelRight.symbol)
|
||||||
}
|
}
|
||||||
enabled: d.isCustomView
|
enabled: d.isCustomView
|
||||||
expectedRoles: ["symbol"]
|
expectedRoles: ["symbol"]
|
||||||
|
@ -175,6 +175,7 @@ ColumnLayout {
|
||||||
Layout.preferredHeight: root.filterVisible ? implicitHeight : 0
|
Layout.preferredHeight: root.filterVisible ? implicitHeight : 0
|
||||||
spacing: 20
|
spacing: 20
|
||||||
opacity: root.filterVisible ? 1 : 0
|
opacity: root.filterVisible ? 1 : 0
|
||||||
|
visible: opacity > 0
|
||||||
|
|
||||||
Behavior on Layout.preferredHeight { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } }
|
Behavior on Layout.preferredHeight { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } }
|
||||||
Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } }
|
Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } }
|
||||||
|
|
Loading…
Reference in New Issue