feat: Manage tokens panel UI component & model

- implements the UI component to manage regular & community tokens
(drag'n'drop or context menu to reorder, show/hide, group by community)
- implements a custom C++ QAIM model which acts as a fake proxy model
for the above QML panel (internally it does all the
sorting/grouping/hiding and preserves the custom sort order)
- adds and corrects support for cascading submenus in StatusAction, and
StatusMenu[Item]
- adds support for mirrored (horizontally flipped) StatusSwitch
- adds a new SortOrderComboBox.qml (this was being used in the first
Figma version, can be ignored now, will be used by the main wallet view
later)
- some minor fixes and cleanups in the used components

Iterates #12377
Closes #12587
This commit is contained in:
Lukáš Tinkl 2023-10-17 21:09:45 +02:00 committed by Lukáš Tinkl
parent d73c51d380
commit 7183621369
28 changed files with 2012 additions and 51 deletions

View File

@ -123,6 +123,7 @@ add_test(NAME QmlTests COMMAND QmlTests -platform offscreen)
list(APPEND QML_DIRS "${CMAKE_SOURCE_DIR}/../ui/app")
list(APPEND QML_DIRS "${CMAKE_SOURCE_DIR}/../ui/imports")
list(APPEND QML_DIRS "${CMAKE_SOURCE_DIR}/../ui/StatusQ/src")
list(APPEND QML_DIRS "${CMAKE_SOURCE_DIR}/src")
list(APPEND QML_DIRS "${CMAKE_SOURCE_DIR}/pages")
list(APPEND QML_DIRS "${CMAKE_SOURCE_DIR}/stubs")

View File

@ -0,0 +1,87 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import StatusQ.Core 0.1
import AppLayouts.Wallet.panels 1.0
import utils 1.0
import Storybook 1.0
import Models 1.0
SplitView {
id: root
Logs { id: logs }
orientation: Qt.Horizontal
ManageTokensModel {
id: assetsModel
}
StatusScrollView { // wrapped in a ScrollView on purpose; to simulate SettingsContentBase.qml
SplitView.fillWidth: true
SplitView.preferredHeight: 500
ManageTokensPanel {
id: showcasePanel
width: 500
baseModel: ctrlEmptyModel.checked ? null : assetsModel
}
}
LogsAndControlsPanel {
id: logsAndControlsPanel
SplitView.minimumWidth: 150
SplitView.preferredWidth: 250
logsView.logText: logs.logText
ColumnLayout {
Label {
Layout.fillWidth: true
text: "Dirty: %1".arg(showcasePanel.dirty ? "true" : "false")
}
Button {
text: "Save"
onClicked: showcasePanel.saveSettings()
}
Button {
enabled: showcasePanel.dirty
text: "Revert"
onClicked: showcasePanel.revert()
}
Button {
text: "Random data"
onClicked: {
assetsModel.clear()
assetsModel.randomizeData()
}
}
Button {
text: "Clear settings"
onClicked: showcasePanel.clearSettings()
}
Switch {
id: ctrlEmptyModel
text: "Empty model"
}
}
}
}
// category: Panels
// https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?type=design&node-id=18139-95033&mode=design&t=nqFScWLfusXBNQA5-0
// https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?type=design&node-id=17674-273051&mode=design&t=nqFScWLfusXBNQA5-0
// https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?type=design&node-id=17636-249780&mode=design&t=nqFScWLfusXBNQA5-0
// https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?type=design&node-id=17674-276833&mode=design&t=nqFScWLfusXBNQA5-0
// https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?type=design&node-id=17675-283206&mode=design&t=nqFScWLfusXBNQA5-0

View File

@ -0,0 +1,237 @@
import QtQuick 2.15
import QtQml.Models 2.15
import Models 1.0
ListModel {
function randomizeData() {
var data = []
for (let i = 0; i < 100; i++) {
const communityId = i % 2 == 0 ? "" : "communityId%1".arg(Math.round(i))
const enabledNetworkBalance = !!communityId ? Math.round(i)
: {
amount: 1,
symbol: "ZRX"
}
var obj = {
name: "Item %1".arg(i),
symbol: "SYM %1".arg(i),
enabledNetworkBalance: enabledNetworkBalance,
enabledNetworkCurrencyBalance: {
amount: 10.37,
symbol: "EUR",
displayDecimals: 2
},
communityId: communityId,
communityName: "COM %1".arg(i),
communityImage: ""
}
data.push(obj)
}
append(data)
}
readonly property var data: [
{
name: "0x",
symbol: "ZRX",
enabledNetworkBalance: {
amount: 1,
symbol: "ZRX"
},
enabledNetworkCurrencyBalance: {
amount: 10.37,
symbol: "EUR",
displayDecimals: 2
},
communityId: "ddls",
communityName: "Doodles",
communityImage: ModelsData.collectibles.doodles // FIXME backend
},
{
name: "Omg",
symbol: "OMG",
enabledNetworkBalance: {
amount: 2,
symbol: "OMG"
},
enabledNetworkCurrencyBalance: {
amount: 13.37,
symbol: "EUR",
displayDecimals: 2
},
communityId: "sox",
communityName: "Socks",
communityImage: ModelsData.icons.socks
},
{
name: "Decentraland",
symbol: "MANA",
enabledNetworkBalance: {
amount: 301,
symbol: "MANA"
},
enabledNetworkCurrencyBalance: {
amount: 75.256,
symbol: "EUR",
displayDecimals: 2
},
changePct24hour: -2.1,
communityId: "",
communityName: "",
communityImage: ""
},
{
name: "Ave Maria",
symbol: "AAVE",
enabledNetworkBalance: {
amount: 23.3,
symbol: "AAVE",
displayDecimals: 2
},
enabledNetworkCurrencyBalance: {
amount: 2.335,
symbol: "EUR",
displayDecimals: 2
},
changePct24hour: 4.56,
communityId: "",
communityName: "",
communityImage: ""
},
{
name: "Polymorphism",
symbol: "POLY",
enabledNetworkBalance: {
amount: 3590,
symbol: "POLY"
},
enabledNetworkCurrencyBalance: {
amount: 2.7,
symbol: "EUR",
displayDecimals: 2
},
changePct24hour: -11.6789,
communityId: "",
communityName: "",
communityImage: ""
},
{
name: "Dai",
symbol: "DAI",
enabledNetworkBalance: {
amount: 634.22,
symbol: "DAI",
displayDecimals: 2
},
enabledNetworkCurrencyBalance: {
amount: 594.72,
symbol: "EUR",
displayDecimals: 2
},
changePct24hour: 0,
communityId: "",
communityName: "",
communityImage: ""
},
{
name: "Makers' choice",
symbol: "MKR",
enabledNetworkBalance: {
amount: 1.3,
symbol: "MKR",
displayDecimals: 2
},
enabledNetworkCurrencyBalance: {
amount: 100.37,
symbol: "EUR",
displayDecimals: 2
},
changePct24hour: -1,
communityId: "",
communityName: "",
communityImage: ""
},
{
name: "Ethereum",
symbol: "ETH",
enabledNetworkBalance: {
amount: 0.12345,
symbol: "ETH",
displayDecimals: 8,
stripTrailingZeroes: true
},
enabledNetworkCurrencyBalance: {
amount: 182.72,
symbol: "EUR",
displayDecimals: 2
},
changePct24hour: -3.51,
communityId: "",
communityName: "",
communityImage: ""
},
{
name: "GetOuttaHere",
symbol: "InvisibleSYM",
enabledNetworkBalance: {},
enabledNetworkCurrencyBalance: {},
changePct24hour: NaN,
communityId: "",
communityName: "",
communityImage: ""
},
{
enabledNetworkBalance: ({
displayDecimals: true,
stripTrailingZeroes: true,
amount: 324343.3,
symbol: "SNT"
}),
enabledNetworkCurrencyBalance: ({
displayDecimals: 4,
stripTrailingZeroes: true,
amount: 2.333321323400,
symbol: "EUR"
}),
symbol: "SNT",
name: "Status",
communityId: "",
communityName: "",
communityImage: ""
},
{
name: "Meth",
symbol: "MET",
enabledNetworkBalance: {
amount: 666,
symbol: "MET"
},
enabledNetworkCurrencyBalance: {
amount: 1000.37,
symbol: "EUR",
displayDecimals: 2
},
communityId: "ddls",
communityName: "Doodles",
communityImage: ModelsData.collectibles.doodles
},
{
name: "Ast",
symbol: "AST",
enabledNetworkBalance: {
amount: 1,
symbol: "AST"
},
enabledNetworkCurrencyBalance: {
amount: 0.374,
symbol: "EUR",
displayDecimals: 2
},
communityId: "ast",
communityName: "Astafarians",
communityImage: ModelsData.icons.dribble
}
]
Component.onCompleted: append(data)
}

View File

@ -9,6 +9,7 @@ FlatTokensModel 1.0 FlatTokensModel.qml
IconModel 1.0 IconModel.qml
LinkPreviewModel 1.0 LinkPreviewModel.qml
MintedTokensModel 1.0 MintedTokensModel.qml
ManageTokensModel 1.0 ManageTokensModel.qml
RecipientModel 1.0 RecipientModel.qml
SourceOfTokensModel 1.0 SourceOfTokensModel.qml
TokenHoldersModel 1.0 TokenHoldersModel.qml

View File

@ -109,6 +109,12 @@ add_library(StatusQ SHARED
src/statuswindow.cpp
src/stringutilsinternal.cpp
src/submodelproxymodel.cpp
# wallet
src/wallet/managetokenscontroller.cpp
src/wallet/managetokenscontroller.h
src/wallet/managetokensmodel.cpp
src/wallet/managetokensmodel.h
)
set_target_properties(StatusQ PROPERTIES

View File

@ -146,9 +146,14 @@ ItemDelegate {
property int visualIndex
/*!
\qmlproperty bool StatusDraggableListItem::draggable
This property holds whether this item can be dragged (and whether the drag handle is displayed)
This property holds whether the drag handle is displayed
*/
property bool draggable
/*!
\qmlproperty bool StatusDraggableListItem::dragEnabled
This property holds whether this item can be dragged (and whether the drag handle is displayed)
*/
property bool dragEnabled: draggable
/*!
\qmlproperty bool StatusDraggableListItem::customizable
This property holds whether this item can be customized
@ -200,6 +205,13 @@ ItemDelegate {
*/
property color bgColor: "transparent"
/*!
\qmlproperty color StatusDraggableListItem::assetBgColor
This property holds icon/image background color, if any
Defaults to "transparent" (ie no background)
*/
property color assetBgColor: "transparent"
Drag.dragType: Drag.Automatic
Drag.hotSpot.x: dragHandler.mouseX
Drag.hotSpot.y: dragHandler.mouseY
@ -209,7 +221,7 @@ ItemDelegate {
\qmlproperty readonly bool StatusDraggableListItem::dragActive
This property holds whether a drag is currently in progress
*/
readonly property bool dragActive: draggable && dragHandler.drag.active
readonly property bool dragActive: dragHandler.drag.active
onDragActiveChanged: {
if (dragActive)
Drag.start()
@ -234,19 +246,25 @@ ItemDelegate {
]
background: Rectangle {
color: root.dragActive && !root.customizable ? Theme.palette.indirectColor2 : "transparent"
color: root.dragActive && !root.customizable ? Theme.palette.alphaColor(Theme.palette.baseColor2, 0.7) : root.bgColor
border.width: root.customizable ? 0 : 1
border.color: Theme.palette.baseColor2
radius: customizable ? 0 : 8
radius: root.customizable ? 0 : 8
MouseArea {
id: dragHandler
anchors.fill: parent
drag.target: root.draggable ? root : null
drag.target: root.dragEnabled ? root : null
drag.axis: root.dragAxis
preventStealing: true // otherwise DND is broken inside a Flickable/ScrollView
hoverEnabled: true
cursorShape: root.dragActive ? Qt.ClosedHandCursor : Qt.OpenHandCursor
cursorShape: {
if (!root.enabled)
return undefined
if (root.dragEnabled)
return root.dragActive ? Qt.ClosedHandCursor : Qt.OpenHandCursor
return Qt.PointingHandCursor
}
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
root.clicked(mouse);
@ -255,8 +273,8 @@ ItemDelegate {
}
// inset to simulate spacing
topInset: 6
bottomInset: 6
topInset: 4
bottomInset: 4
horizontalPadding: 12
verticalPadding: 16
@ -273,10 +291,10 @@ ItemDelegate {
Layout.preferredHeight: 20
icon: "justify"
visible: root.draggable && !root.customizable
color: root.dragEnabled ? Theme.palette.baseColor1 : Theme.palette.baseColor2
}
Loader {
Layout.leftMargin: root.spacing/2
asynchronous: true
active: !!root.icon.name || !!root.icon.source
visible: active
@ -293,7 +311,7 @@ ItemDelegate {
visible: text
elide: Text.ElideRight
maximumLineCount: 1
font.weight: root.highlighted ? Font.Medium : Font.Normal
font.weight: Font.Medium
}
Row {
@ -302,6 +320,8 @@ ItemDelegate {
spacing: 8
StatusBaseText {
width: Math.min(parent.width - (secondaryTitleIconLoader.item ? parent.spacing + secondaryTitleIconLoader.item.width : 0),
implicitWidth)
text: root.secondaryTitle
color: Theme.palette.baseColor1
elide: Text.ElideRight
@ -309,6 +329,8 @@ ItemDelegate {
}
Loader {
id: secondaryTitleIconLoader
anchors.verticalCenter: parent.verticalCenter
asynchronous: true
active: !!root.secondaryTitleIcon
visible: active
@ -349,13 +371,10 @@ ItemDelegate {
id: imageComponent
StatusRoundedImage {
radius: root.bgRadius
color: root.bgColor
color: root.assetBgColor
width: root.icon.width
height: root.icon.height
image.source: root.icon.source
image.sourceSize: Qt.size(width, height)
image.smooth: false
image.mipmap: true
showLoadingIndicator: true
image.fillMode: Image.PreserveAspectCrop
}

View File

@ -74,7 +74,6 @@ CheckBox {
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
width: parent.width
color: Theme.palette.directColor1
lineHeight: 1.2
leftPadding: root.leftSide? (!!root.text ? root.indicator.width + root.spacing
: root.indicator.width) : 0

View File

@ -18,6 +18,9 @@ ItemDelegate {
icon.width: 16
icon.height: 16
font.family: Theme.palette.baseFont.name
font.pixelSize: 15
contentItem: RowLayout {
spacing: root.spacing

View File

@ -8,6 +8,8 @@ import StatusQ.Components 0.1
Switch {
id: root
property color textColor: Theme.palette.directColor1
background: MouseArea {
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.NoButton
@ -18,8 +20,9 @@ Switch {
implicitWidth: 52
implicitHeight: 28
x: root.leftPadding
y: parent.height / 2 - height / 2
anchors.left: parent.left
anchors.leftMargin: root.leftPadding
anchors.verticalCenter: parent.verticalCenter
Rectangle {
anchors.fill: parent
@ -71,8 +74,9 @@ Switch {
contentItem: StatusBaseText {
text: root.text
opacity: enabled ? 1.0 : 0.3
color: root.textColor
verticalAlignment: Text.AlignVCenter
leftPadding: !!root.text ? root.indicator.width + root.spacing
: root.indicator.width
leftPadding: root.mirrored ? 0 : !!root.text ? root.indicator.width + root.spacing : root.indicator.width
rightPadding: root.mirrored ? !!root.text ? root.indicator.width + root.spacing : root.indicator.width : 0
}
}

View File

@ -23,6 +23,7 @@ Action {
imgIsIdenticon: false
color: root.icon.color
name: root.icon.name
hoverColor: Theme.palette.statusMenu.hoverBackgroundColor
}
property StatusFontSettings fontSettings: StatusFontSettings {}

View File

@ -36,13 +36,23 @@ Menu {
property real maxImplicitWidth: 640
readonly property color defaultIconColor: Theme.palette.primaryColor1
property int type: StatusAction.Type.Normal
property StatusAssetSettings assetSettings: StatusAssetSettings {
width: 18
height: 18
rotation: 0
isLetterIdenticon: false
isImage: false
color: root.defaultIconColor
color: {
if (!root.enabled)
return Theme.palette.baseColor1
if (root.type === StatusAction.Type.Danger)
return Theme.palette.dangerColor1
if (root.type === StatusAction.Type.Success)
return Theme.palette.successColor1
return Theme.palette.primaryColor1
}
}
property StatusFontSettings fontSettings: StatusFontSettings {}
@ -57,8 +67,6 @@ Menu {
property var openHandler
property var closeHandler
signal menuItemClicked(int menuIndex)
function checkIfEmpty() {
for (let i = 0; i < root.contentItem.count; ++i) {
const menuItem = root.contentItem.itemAtIndex(i)
@ -98,6 +106,7 @@ Menu {
visible: root.hideDisabledItems ? enabled : true
height: visible ? implicitHeight : 0
onImplicitWidthChanged: {
if (visible)
d.maxDelegateImplWidth = Math.max(d.maxDelegateImplWidth, implicitWidth)
}
}

View File

@ -23,8 +23,10 @@ MenuItem {
readonly property bool subMenuOpened: isSubMenu && root.subMenu.opened
readonly property bool hasAction: !!root.action
readonly property bool isStatusAction: d.hasAction && (root.action instanceof StatusAction)
readonly property bool isStatusDangerAction: d.isStatusAction && root.action.type === StatusAction.Type.Danger
readonly property bool isStatusSuccessAction: d.isStatusAction && root.action.type === StatusAction.Type.Success
readonly property bool isStatusDangerAction: (d.isStatusAction && root.action.type === StatusAction.Type.Danger) ||
(d.isStatusSubMenu && root.subMenu.type === StatusAction.Type.Danger)
readonly property bool isStatusSuccessAction: (d.isStatusAction && root.action.type === StatusAction.Type.Success) ||
(d.isStatusSubMenu && root.subMenu.type === StatusAction.Type.Success)
readonly property StatusAssetSettings originalAssetSettings: d.isStatusSubMenu && root.subMenu.assetSettings
? root.subMenu.assetSettings

View File

@ -1,10 +1,6 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.12
import StatusQ.Components 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
StatusAction {
id: root

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.0964 9.05604L10.6 13.1437V2.5H9.39996V13.1436L4.90361 9.05604L4.09641 9.94396L9.59641 14.944L10 15.3109L10.4036 14.944L15.9036 9.94396L15.0964 9.05604ZM3 17.6H17V16.4H3V17.6Z" fill="#09101C"/>
</svg>

After

Width:  |  Height:  |  Size: 349 B

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
version="1.1"
id="svg1"
sodipodi:docname="arrow-top.svg"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="38.25"
inkscape:cx="10"
inkscape:cy="10"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 4.9036,11.04396 9.4,6.9563 V 17.6 h 1.20004 V 6.9564 l 4.49635,4.08756 0.8072,-0.88792 -5.5,-5.00004 L 10,4.7891 9.5964,5.156 l -5.5,5.00004 z M 17,2.5 H 3 v 1.2 h 14 z"
fill="#09101c"
id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -15,6 +15,9 @@
#include "StatusQ/submodelproxymodel.h"
#include "wallet/managetokenscontroller.h"
#include "wallet/managetokensmodel.h"
class StatusQPlugin : public QQmlExtensionPlugin
{
Q_OBJECT
@ -28,6 +31,9 @@ public:
qmlRegisterType<StatusSyntaxHighlighter>("StatusQ", 0, 1, "StatusSyntaxHighlighter");
qmlRegisterType<RXValidator>("StatusQ", 0, 1, "RXValidator");
qmlRegisterType<ManageTokensController>("StatusQ.Models", 0, 1, "ManageTokensController");
qmlRegisterType<ManageTokensModel>("StatusQ.Models", 0, 1, "ManageTokensModel");
qmlRegisterType<LeftJoinModel>("StatusQ", 0, 1, "LeftJoinModel");
qmlRegisterType<SubmodelProxyModel>("StatusQ", 0, 1, "SubmodelProxyModel");
qmlRegisterType<RoleRename>("StatusQ", 0, 1, "RoleRename");

View File

@ -0,0 +1,393 @@
#include "managetokenscontroller.h"
#include <QElapsedTimer>
ManageTokensController::ManageTokensController(QObject* parent)
: QObject(parent)
, m_regularTokensModel(new ManageTokensModel(this))
, m_communityTokensModel(new ManageTokensModel(this))
, m_communityTokenGroupsModel(new ManageTokensModel(this))
, m_hiddenTokensModel(new ManageTokensModel(this))
{
for (auto model : m_allModels) {
connect(model, &ManageTokensModel::dirtyChanged, this, &ManageTokensController::dirtyChanged);
}
connect(this, &ManageTokensController::sourceModelChanged, this, [this]() {
if (!m_sourceModel) {
m_modelConnectionsInitialized = false;
return;
}
if (m_modelConnectionsInitialized)
return;
connect(m_sourceModel, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int first, int last) {
#ifdef QT_DEBUG
QElapsedTimer t;
t.start();
qCInfo(manageTokens) << "!!! ADDING" << last-first+1 << "NEW TOKENS";
#endif
for (int i = first; i <= last; i++)
addItem(i);
reloadCommunityIds();
m_communityTokensModel->setCommunityIds(m_communityIds);
m_communityTokensModel->saveCustomSortOrder();
rebuildCommunityTokenGroupsModel();
#ifdef QT_DEBUG
qCInfo(manageTokens) << "!!! ADDING NEW SOURCE DATA TOOK" << t.nsecsElapsed()/1'000'000.f << "ms";
#endif
});
connect(m_sourceModel, &QAbstractItemModel::rowsRemoved, this, &ManageTokensController::parseSourceModel);
connect(m_sourceModel, &QAbstractItemModel::dataChanged, this, &ManageTokensController::parseSourceModel); // NB at this point we don't know in which submodel the item is
connect(m_communityTokensModel, &ManageTokensModel::rowsMoved, this, [this]() {
if (!m_arrangeByCommunity)
rebuildCommunityTokenGroupsModel();
reloadCommunityIds();
m_communityTokensModel->setCommunityIds(m_communityIds);
m_communityTokensModel->saveCustomSortOrder();
});
connect(m_communityTokenGroupsModel, &ManageTokensModel::rowsMoved, this, [this](const QModelIndex &parent, int start, int end, const QModelIndex &destination, int toRow) {
qCDebug(manageTokens) << "!!! GROUP MOVED FROM" << start << "TO" << toRow;
// FIXME swap toRow<->start instead of reloadCommunityIds()?
reloadCommunityIds();
m_communityTokensModel->setCommunityIds(m_communityIds);
m_communityTokensModel->saveCustomSortOrder();
});
m_modelConnectionsInitialized = true;
});
}
void ManageTokensController::showHideRegularToken(int row, bool flag)
{
if (flag) { // show
auto hiddenItem = m_hiddenTokensModel->takeItem(row);
if (hiddenItem)
m_regularTokensModel->addItem(*hiddenItem);
} else { // hide
auto shownItem = m_regularTokensModel->takeItem(row);
if (shownItem)
m_hiddenTokensModel->addItem(*shownItem, false /*prepend*/);
}
}
void ManageTokensController::showHideCommunityToken(int row, bool flag)
{
if (flag) { // show
auto hiddenItem = m_hiddenTokensModel->takeItem(row);
if (hiddenItem) {
m_communityTokensModel->addItem(*hiddenItem);
if (!m_communityIds.contains(hiddenItem->communityId))
m_communityIds.append(hiddenItem->communityId);
}
} else { // hide
auto shownItem = m_communityTokensModel->takeItem(row);
if (shownItem) {
m_hiddenTokensModel->addItem(*shownItem, false /*prepend*/);
if (!m_communityTokensModel->hasCommunityIdToken(shownItem->communityId))
m_communityIds.removeAll(shownItem->communityId);
}
}
m_communityTokensModel->setCommunityIds(m_communityIds);
m_communityTokensModel->saveCustomSortOrder();
rebuildCommunityTokenGroupsModel();
}
void ManageTokensController::showHideGroup(const QString& groupId, bool flag)
{
if (flag) { // show
const auto tokens = m_hiddenTokensModel->takeAllItems(groupId);
for (const auto& token: tokens) {
m_communityTokensModel->addItem(token);
}
m_communityIds.append(groupId);
} else { // hide
const auto tokens = m_communityTokensModel->takeAllItems(groupId);
for (const auto& token: tokens) {
m_hiddenTokensModel->addItem(token, false /*prepend*/);
}
m_communityIds.removeAll(groupId);
}
m_communityTokensModel->setCommunityIds(m_communityIds);
m_communityTokensModel->saveCustomSortOrder();
rebuildCommunityTokenGroupsModel();
}
void ManageTokensController::saveSettings()
{
Q_ASSERT(!m_settingsKey.isEmpty());
// gather the data to save
SerializedTokenData result;
for (auto model: {m_regularTokensModel, m_communityTokensModel})
result.insert(model->save());
result.insert(m_hiddenTokensModel->save(false));
// save to QSettings
m_settings.beginGroup(QStringLiteral("ManageTokens-%1").arg(m_settingsKey));
m_settings.beginWriteArray(m_settingsKey);
SerializedTokenData::const_key_value_iterator it = result.constKeyValueBegin();
for (auto i = 0; it != result.constKeyValueEnd() && i < result.size(); it++, i++) {
m_settings.setArrayIndex(i);
const auto tuple = it->second;
m_settings.setValue(QStringLiteral("symbol"), it->first);
m_settings.setValue(QStringLiteral("pos"), std::get<0>(tuple));
m_settings.setValue(QStringLiteral("visible"), std::get<1>(tuple));
m_settings.setValue(QStringLiteral("groupId"), std::get<2>(tuple));
}
m_settings.endArray();
m_settings.endGroup();
m_settings.sync();
// unset dirty
for (auto model: m_allModels)
model->setDirty(false);
}
void ManageTokensController::clearSettings()
{
Q_ASSERT(!m_settingsKey.isEmpty());
// clear the relevant QSettings group
m_settings.beginGroup(QStringLiteral("ManageTokens-%1").arg(m_settingsKey));
m_settings.remove(QString());
m_settings.endGroup();
m_settings.sync();
}
void ManageTokensController::loadSettings()
{
Q_ASSERT(!m_settingsKey.isEmpty());
m_settingsData.clear();
// load from QSettings
m_settings.beginGroup(QStringLiteral("ManageTokens-%1").arg(m_settingsKey));
const auto size = m_settings.beginReadArray(m_settingsKey);
for (auto i = 0; i < size; i++) {
m_settings.setArrayIndex(i);
const auto symbol = m_settings.value(QStringLiteral("symbol")).toString();
if (symbol.isEmpty()) {
qCWarning(manageTokens) << Q_FUNC_INFO << "Missing symbol while reading tokens settings";
continue;
}
const auto pos = m_settings.value(QStringLiteral("pos"), -1).toInt();
const auto visible = m_settings.value(QStringLiteral("visible"), true).toBool();
const auto groupId = m_settings.value(QStringLiteral("groupId")).toString();
m_settingsData.insert(symbol, {pos, visible, groupId});
}
m_settings.endArray();
m_settings.endGroup();
}
void ManageTokensController::revert()
{
loadSettings();
parseSourceModel();
}
void ManageTokensController::classBegin()
{
// empty on purpose
}
void ManageTokensController::componentComplete()
{
loadSettings();
}
void ManageTokensController::setSourceModel(QAbstractItemModel* newSourceModel)
{
if(m_sourceModel == newSourceModel) return;
if(!newSourceModel) {
disconnect(sourceModel());
// clear all the models
for (auto model: m_allModels)
model->clear();
m_communityIds.clear();
m_sourceModel = newSourceModel;
emit sourceModelChanged();
return;
}
m_sourceModel = newSourceModel;
connect(m_sourceModel, &QAbstractItemModel::modelReset, this, &ManageTokensController::parseSourceModel);
if (m_sourceModel && m_sourceModel->roleNames().isEmpty()) { // workaround for when a model has no roles and roles are added when the model is populated (ListModel)
// QTBUG-57971
connect(m_sourceModel, &QAbstractItemModel::rowsInserted, this, &ManageTokensController::parseSourceModel);
return;
} else {
parseSourceModel();
}
}
void ManageTokensController::parseSourceModel()
{
if (!m_sourceModel)
return;
disconnect(m_sourceModel, &QAbstractItemModel::rowsInserted, this, &ManageTokensController::parseSourceModel);
#ifdef QT_DEBUG
QElapsedTimer t;
t.start();
#endif
// clear all the models
for (auto model: m_allModels)
model->clear();
m_communityIds.clear();
// read and transform the original data
const auto newSize = m_sourceModel->rowCount();
qCInfo(manageTokens) << "!!! PARSING" << newSize << "TOKENS";
for (auto i = 0; i < newSize; i++) {
addItem(i);
}
// build community groups model
rebuildCommunityTokenGroupsModel();
reloadCommunityIds();
m_communityTokensModel->setCommunityIds(m_communityIds);
// (pre)sort
for (auto model: m_allModels) {
model->applySort();
model->saveCustomSortOrder();
model->setDirty(false);
}
#ifdef QT_DEBUG
qCInfo(manageTokens) << "!!! PARSING SOURCE DATA TOOK" << t.nsecsElapsed()/1'000'000.f << "ms";
#endif
emit sourceModelChanged();
}
void ManageTokensController::addItem(int index)
{
const auto sourceRoleNames = m_sourceModel->roleNames();
const auto dataForIndex = [&](const QModelIndex &idx, const QByteArray& rolename) -> QVariant {
const auto key = sourceRoleNames.key(rolename, -1);
if (key == -1)
return {};
return idx.data(key);
};
const auto srcIndex = m_sourceModel->index(index, 0);
const auto symbol = dataForIndex(srcIndex, kSymbolRoleName).toString();
const auto communityId = dataForIndex(srcIndex, kCommunityIdRoleName).toString();
const auto communityName = dataForIndex(srcIndex, kCommunityNameRoleName).toString();
const auto visible = m_settingsData.contains(symbol) ? std::get<1>(m_settingsData.value(symbol)) : true;
TokenData token;
token.symbol = symbol;
token.name = dataForIndex(srcIndex, kNameRoleName).toString();
token.image = dataForIndex(srcIndex, kTokenImageRoleName).toString();
token.communityId = communityId;
token.communityName = !communityName.isEmpty() ? communityName : communityId;
token.communityImage = dataForIndex(srcIndex, kCommunityImageRoleName).toString();
token.collectionUid = dataForIndex(srcIndex, kCollectionUidRoleName).toString();
token.collectionName = dataForIndex(srcIndex, kCollectionNameRoleName).toString();
token.balance = dataForIndex(srcIndex, kEnabledNetworkBalanceRoleName);
token.currencyBalance = dataForIndex(srcIndex, kEnabledNetworkCurrencyBalanceRoleName);
token.customSortOrderNo = m_settingsData.contains(symbol) ? std::get<0>(m_settingsData.value(symbol))
: (visible ? INT_MAX : 0); // append/prepend
if (!visible)
m_hiddenTokensModel->addItem(token, /*append*/ false);
else if (!communityId.isEmpty())
m_communityTokensModel->addItem(token);
else
m_regularTokensModel->addItem(token);
}
bool ManageTokensController::dirty() const
{
return std::any_of(m_allModels.cbegin(), m_allModels.cend(), [](auto model) {
return model->dirty();
});
}
bool ManageTokensController::arrangeByCommunity() const
{
return m_arrangeByCommunity;
}
void ManageTokensController::setArrangeByCommunity(bool newArrangeByCommunity)
{
if(m_arrangeByCommunity == newArrangeByCommunity) return;
m_arrangeByCommunity = newArrangeByCommunity;
if (!m_arrangeByCommunity)
m_communityTokensModel->applySort();
else
rebuildCommunityTokenGroupsModel();
emit arrangeByCommunityChanged();
}
void ManageTokensController::reloadCommunityIds()
{
m_communityIds.clear();
auto model = m_arrangeByCommunity ? m_communityTokenGroupsModel : m_communityTokensModel;
const auto count = model->count();
for (int i = 0; i < count; i++) {
const auto& token = model->itemAt(i);
if (!m_communityIds.contains(token.communityId))
m_communityIds.append(token.communityId);
}
qCDebug(manageTokens) << "!!! FOUND UNIQUE COMMUNITY GROUP IDs:" << m_communityIds;
}
void ManageTokensController::rebuildCommunityTokenGroupsModel()
{
QStringList communityIds;
QList<TokenData> result;
const auto count = m_communityTokensModel->count();
for (auto i = 0; i < count; i++) {
const auto& communityToken = m_communityTokensModel->itemAt(i);
const auto communityId = communityToken.communityId;
if (!communityIds.contains(communityId)) { // insert into groups
communityIds.append(communityId);
TokenData tokenGroup;
tokenGroup.communityId = communityId;
tokenGroup.communityName = communityToken.communityName;
tokenGroup.communityImage = communityToken.communityImage;
tokenGroup.balance = 1;
result.append(tokenGroup);
} else { // update group's childCount
const auto tokenGroup = std::find_if(result.cbegin(), result.cend(), [communityId](const auto& item) {
return communityId == item.communityId;
});
if (tokenGroup != result.cend()) {
const auto row = std::distance(result.cbegin(), tokenGroup);
TokenData updTokenGroup = result.takeAt(row);
updTokenGroup.balance = updTokenGroup.balance.toInt() + 1;
result.insert(row, updTokenGroup);
}
}
}
m_communityTokenGroupsModel->clear();
for (const auto& group: result)
m_communityTokenGroupsModel->addItem(group);
qCDebug(manageTokens) << "!!! GROUPS MODEL REBUILT WITH GROUPS:" << communityIds;
}
QString ManageTokensController::settingsKey() const
{
return m_settingsKey;
}
void ManageTokensController::setSettingsKey(const QString& newSettingsKey)
{
if (m_settingsKey == newSettingsKey)
return;
m_settingsKey = newSettingsKey;
emit settingsKeyChanged();
}

View File

@ -0,0 +1,94 @@
#include <QObject>
#include <QQmlParserStatus>
#include <QSettings>
#include <array>
#include "managetokensmodel.h"
class QAbstractItemModel;
class ManageTokensController : public QObject, public QQmlParserStatus
{
Q_OBJECT
Q_INTERFACES(QQmlParserStatus)
// input properties
Q_PROPERTY(QAbstractItemModel* sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged FINAL REQUIRED)
Q_PROPERTY(QString settingsKey READ settingsKey WRITE setSettingsKey NOTIFY settingsKeyChanged FINAL REQUIRED)
Q_PROPERTY(bool arrangeByCommunity READ arrangeByCommunity WRITE setArrangeByCommunity NOTIFY arrangeByCommunityChanged FINAL)
// output properties
Q_PROPERTY(QAbstractItemModel* regularTokensModel READ regularTokensModel CONSTANT FINAL)
// TODO regularTokenGroupsModel for grouped (collections of) collectibles?
Q_PROPERTY(QAbstractItemModel* communityTokensModel READ communityTokensModel CONSTANT FINAL)
Q_PROPERTY(QAbstractItemModel* communityTokenGroupsModel READ communityTokenGroupsModel CONSTANT FINAL)
Q_PROPERTY(QAbstractItemModel* hiddenTokensModel READ hiddenTokensModel CONSTANT FINAL)
Q_PROPERTY(bool dirty READ dirty NOTIFY dirtyChanged FINAL)
public:
explicit ManageTokensController(QObject* parent = nullptr);
Q_INVOKABLE void showHideRegularToken(int row, bool flag);
Q_INVOKABLE void showHideCommunityToken(int row, bool flag);
Q_INVOKABLE void showHideGroup(const QString& groupId, bool flag);
Q_INVOKABLE void saveSettings();
Q_INVOKABLE void clearSettings();
Q_INVOKABLE void revert();
// TODO: to be used by SFPM on the main wallet page as an "expressionRole"
// bool lessThan(lhsSymbol, rhsSymbol) const;
// bool filterAcceptsRow(index or symbol?) const;
protected:
void classBegin() override;
void componentComplete() override;
signals:
void sourceModelChanged();
void dirtyChanged();
void arrangeByCommunityChanged();
void settingsKeyChanged();
private:
QAbstractItemModel* m_sourceModel{nullptr};
QAbstractItemModel* sourceModel() const { return m_sourceModel; }
void setSourceModel(QAbstractItemModel* newSourceModel);
void parseSourceModel();
void addItem(int index);
ManageTokensModel* m_regularTokensModel{nullptr};
QAbstractItemModel* regularTokensModel() const { return m_regularTokensModel; };
ManageTokensModel* m_communityTokensModel{nullptr};
QAbstractItemModel* communityTokensModel() const { return m_communityTokensModel; };
ManageTokensModel* m_communityTokenGroupsModel{nullptr};
QAbstractItemModel* communityTokenGroupsModel() const { return m_communityTokenGroupsModel; };
ManageTokensModel* m_hiddenTokensModel{nullptr};
QAbstractItemModel* hiddenTokensModel() const { return m_hiddenTokensModel; };
bool dirty() const;
bool m_arrangeByCommunity{false};
bool arrangeByCommunity() const;
void setArrangeByCommunity(bool newArrangeByCommunity);
QStringList m_communityIds;
void reloadCommunityIds();
void rebuildCommunityTokenGroupsModel();
const std::array<ManageTokensModel*, 4> m_allModels {m_regularTokensModel, m_communityTokensModel, m_communityTokenGroupsModel, m_hiddenTokensModel};
QString m_settingsKey;
QString settingsKey() const;
void setSettingsKey(const QString& newSettingsKey);
QSettings m_settings;
void loadSettings();
SerializedTokenData m_settingsData; // symbol -> {sortOrder, visible, groupId}
bool m_modelConnectionsInitialized{false};
};

View File

@ -0,0 +1,195 @@
#include "managetokensmodel.h"
#include <algorithm>
Q_LOGGING_CATEGORY(manageTokens, "status.models.manageTokens", QtInfoMsg)
ManageTokensModel::ManageTokensModel(QObject* parent)
: QAbstractListModel(parent)
{
connect(this, &QAbstractItemModel::rowsInserted, this, &ManageTokensModel::countChanged);
connect(this, &QAbstractItemModel::rowsRemoved, this, &ManageTokensModel::countChanged);
connect(this, &QAbstractItemModel::modelReset, this, &ManageTokensModel::countChanged);
connect(this, &QAbstractItemModel::layoutChanged, this, &ManageTokensModel::countChanged);
}
void ManageTokensModel::moveItem(int fromRow, int toRow)
{
qCDebug(manageTokens) << Q_FUNC_INFO << "from" << fromRow << "to" << toRow;
if (toRow < 0 || toRow >= rowCount() || fromRow < 0 || fromRow >= rowCount())
return;
auto destRow = toRow;
if (toRow > fromRow)
destRow++;
beginMoveRows({}, fromRow, fromRow, {}, destRow);
m_data.move(fromRow, toRow);
endMoveRows();
setDirty(true);
}
void ManageTokensModel::addItem(const TokenData& item, bool append)
{
const auto destRow = append ? rowCount() : 0;
beginInsertRows({}, destRow, destRow);
append ? m_data.append(item) : m_data.prepend(item);
endInsertRows();
setDirty(true);
}
std::optional<TokenData> ManageTokensModel::takeItem(int row)
{
if (row < 0 || row >= rowCount())
return {};
beginRemoveRows({}, row, row);
auto res = m_data.takeAt(row);
endRemoveRows();
setDirty(true);
return res;
}
QList<TokenData> ManageTokensModel::takeAllItems(const QString& communityId)
{
QList<TokenData> result;
QList<int> indexesToRemove;
for (int i = 0; i < m_data.count(); i++) {
const auto &token = m_data.at(i);
if (token.communityId == communityId) {
result.append(token);
indexesToRemove.append(i);
}
}
QList<int>::reverse_iterator its;
for(its = indexesToRemove.rbegin(); its != indexesToRemove.rend(); ++its) {
const auto row = *its;
beginRemoveRows({}, row, row);
m_data.removeAt(row);
endRemoveRows();
}
setDirty(true);
return result;
}
void ManageTokensModel::clear()
{
beginResetModel();
m_data.clear();
endResetModel();
setDirty(false);
}
SerializedTokenData ManageTokensModel::save(bool isVisible)
{
saveCustomSortOrder();
const auto size = count();
SerializedTokenData result;
for (int i = 0; i < size; i++) {
const auto& token = itemAt(i);
const auto groupId = !token.communityId.isEmpty() ? token.communityId : token.collectionUid;
result.insert(token.symbol, {i, isVisible, groupId});
}
setDirty(false);
return result;
}
int ManageTokensModel::rowCount(const QModelIndex& parent) const
{
return m_data.size();
}
QHash<int, QByteArray> ManageTokensModel::roleNames() const
{
static const QHash<int, QByteArray> roles {
{SymbolRole, kSymbolRoleName},
{NameRole, kNameRoleName},
{CommunityIdRole, kCommunityIdRoleName},
{CommunityNameRole, kCommunityNameRoleName},
{CommunityImageRole, kCommunityImageRoleName},
{CollectionUidRole, kCollectionUidRoleName},
{CollectionNameRole, kCollectionNameRoleName},
{BalanceRole, kEnabledNetworkBalanceRoleName},
{CurrencyBalanceRole, kEnabledNetworkCurrencyBalanceRoleName},
{CustomSortOrderNoRole, kCustomSortOrderNoRoleName},
{TokenImageRole, kTokenImageRoleName},
};
return roles;
}
QVariant ManageTokensModel::data(const QModelIndex& index, int role) const
{
if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid | QAbstractItemModel::CheckIndexOption::ParentIsInvalid))
return {};
const auto& token = m_data.at(index.row());
switch(static_cast<TokenDataRoles>(role))
{
case SymbolRole: return token.symbol;
case NameRole: return token.name;
case CommunityIdRole: return token.communityId;
case CommunityNameRole: return token.communityName;
case CommunityImageRole: return token.communityImage;
case CollectionUidRole: return token.collectionUid;
case CollectionNameRole: return token.collectionName;
case BalanceRole: return token.balance;
case CurrencyBalanceRole: return token.currencyBalance;
case CustomSortOrderNoRole: return token.customSortOrderNo;
case TokenImageRole: return token.image;
}
return {};
}
bool ManageTokensModel::dirty() const
{
return m_dirty;
}
void ManageTokensModel::setDirty(bool flag)
{
if (m_dirty == flag) return;
m_dirty = flag;
emit dirtyChanged();
}
void ManageTokensModel::saveCustomSortOrder()
{
const auto count = rowCount();
for (auto i = 0; i < count; i++) {
TokenData newToken{m_data.at(i)};
if (newToken.communityId.isEmpty()) {
newToken.customSortOrderNo = i;
} else {
const auto communityIdx = m_communityIds.indexOf(newToken.communityId) + 1;
newToken.customSortOrderNo = i + (communityIdx * 100'000);
}
m_data[i] = newToken;
}
emit dataChanged(index(0, 0), index(count - 1, 0), {TokenDataRoles::CustomSortOrderNoRole});
}
void ManageTokensModel::applySort()
{
emit layoutAboutToBeChanged({}, QAbstractItemModel::VerticalSortHint);
// clazy:exclude=clazy-detaching-member
std::stable_sort(m_data.begin(), m_data.end(), [this](const TokenData& lhs, const TokenData& rhs) {
return lhs.customSortOrderNo < rhs.customSortOrderNo;
});
emit layoutChanged({}, QAbstractItemModel::VerticalSortHint);
}
bool ManageTokensModel::hasCommunityIdToken(const QString& communityId) const
{
return std::any_of(m_data.cbegin(), m_data.constEnd(), [communityId](const auto& token) {
return token.communityId == communityId;
});
}

View File

@ -0,0 +1,93 @@
#pragma once
#include <QAbstractListModel>
#include <QLoggingCategory>
#include <optional>
Q_DECLARE_LOGGING_CATEGORY(manageTokens)
namespace
{
const auto kSymbolRoleName = QByteArrayLiteral("symbol");
const auto kNameRoleName = QByteArrayLiteral("name");
const auto kCommunityIdRoleName = QByteArrayLiteral("communityId");
const auto kCommunityNameRoleName = QByteArrayLiteral("communityName");
const auto kCommunityImageRoleName = QByteArrayLiteral("communityImage");
const auto kCollectionUidRoleName = QByteArrayLiteral("collectionUid");
const auto kCollectionNameRoleName = QByteArrayLiteral("collectionName");
const auto kEnabledNetworkBalanceRoleName = QByteArrayLiteral("enabledNetworkBalance");
const auto kEnabledNetworkCurrencyBalanceRoleName = QByteArrayLiteral("enabledNetworkCurrencyBalance");
const auto kCustomSortOrderNoRoleName = QByteArrayLiteral("customSortOrderNo");
const auto kTokenImageRoleName = QByteArrayLiteral("imageUrl");
} // namespace
struct TokenData {
QString symbol, name, communityId, communityName, communityImage, collectionUid, collectionName, image;
QVariant balance, currencyBalance;
int customSortOrderNo{-1};
};
// symbol -> {sortOrder, visible, groupId}
using SerializedTokenData = QHash<QString, std::tuple<int, bool, QString>>;
class ManageTokensModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(int count READ count NOTIFY countChanged FINAL)
Q_PROPERTY(bool dirty READ dirty NOTIFY dirtyChanged FINAL)
public:
enum TokenDataRoles {
SymbolRole = Qt::UserRole + 1,
NameRole,
CommunityIdRole,
CommunityNameRole,
CommunityImageRole,
CollectionUidRole,
CollectionNameRole,
BalanceRole,
CurrencyBalanceRole,
CustomSortOrderNoRole,
TokenImageRole,
};
Q_ENUM(TokenDataRoles)
explicit ManageTokensModel(QObject* parent = nullptr);
Q_INVOKABLE void moveItem(int fromRow, int toRow);
void addItem(const TokenData& item, bool append = true);
std::optional<TokenData> takeItem(int row);
QList<TokenData> takeAllItems(const QString& communityId);
void clear();
SerializedTokenData save(bool isVisible = true);
bool dirty() const;
void setDirty(bool flag);
void saveCustomSortOrder();
void applySort();
int count() const { return rowCount(); }
const TokenData& itemAt(int row) const { return m_data.at(row); }
void setCommunityIds(const QStringList& ids) { m_communityIds = ids; };
bool hasCommunityIdToken(const QString& communityId) const;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QHash<int, QByteArray> roleNames() const override;
QVariant data(const QModelIndex &index, int role) const override;
signals:
void countChanged();
void dirtyChanged();
private:
QStringList m_communityIds;
bool m_dirty{false};
QList<TokenData> m_data;
};

View File

@ -9,5 +9,5 @@ ShowcaseDelegate {
icon.source: hasImage ? showcaseObj.imageUrl : ""
bgRadius: Style.current.radius
bgColor: !!showcaseObj && !!showcaseObj.backgroundColor ? showcaseObj.backgroundColor : "transparent"
assetBgColor: !!showcaseObj && !!showcaseObj.backgroundColor ? showcaseObj.backgroundColor : "transparent"
}

View File

@ -90,7 +90,6 @@ StatusDraggableListItem {
}
}
}
}
]
}

View File

@ -0,0 +1,127 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import StatusQ.Core 0.1
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Popups 0.1
StatusFlatButton {
id: root
property int currentIndex
property int count
property bool inHidden
property bool isGroup
property string groupId
property bool isCommunityAsset
readonly property bool hideEnabled: model.symbol !== "ETH"
readonly property bool menuVisible: menuLoader.active
signal moveRequested(int from, int to)
signal showHideRequested(int index, bool flag)
signal showHideGroupRequested(string groupId, bool flag)
icon.name: "more"
horizontalPadding: 4
verticalPadding: 4
textColor: hovered || highlighted ? Theme.palette.directColor1 : Theme.palette.baseColor1
highlighted: menuLoader.item && menuLoader.item.opened
onClicked: {
menuLoader.active = true
menuLoader.item.popup(width - menuLoader.item.width, height)
}
Loader {
id: menuLoader
active: false
sourceComponent: StatusMenu {
onClosed: menuLoader.active = false
StatusAction {
enabled: !root.inHidden && root.currentIndex !== 0
icon.name: "arrow-top"
text: qsTr("Move to top")
onTriggered: root.moveRequested(root.currentIndex, 0)
}
StatusAction {
enabled: !root.inHidden && root.currentIndex !== 0
icon.name: "arrow-up"
text: qsTr("Move up")
onTriggered: root.moveRequested(root.currentIndex, root.currentIndex - 1)
}
StatusAction {
enabled: !root.inHidden && root.currentIndex < root.count - 1
icon.name: "arrow-down"
text: qsTr("Move down")
onTriggered: root.moveRequested(root.currentIndex, root.currentIndex + 1)
}
StatusAction {
enabled: !root.inHidden && root.currentIndex < root.count - 1
icon.name: "arrow-bottom"
text: qsTr("Move to bottom")
onTriggered: root.moveRequested(root.currentIndex, root.count - 1)
}
StatusMenuSeparator { enabled: !root.inHidden && root.hideEnabled }
// any token
StatusAction {
enabled: !root.inHidden && root.hideEnabled && !root.isGroup && !root.isCommunityAsset
type: StatusAction.Type.Danger
icon.name: "hide"
text: qsTr("Hide asset")
onTriggered: root.showHideRequested(root.currentIndex, false)
}
StatusAction {
enabled: root.inHidden
icon.name: "show"
text: qsTr("Show asset")
onTriggered: root.showHideRequested(root.currentIndex, true)
}
// (hide) community tokens
StatusMenu {
id: communitySubmenu
enabled: !root.inHidden && root.isCommunityAsset
title: qsTr("Hide")
assetSettings.name: "hide"
type: StatusAction.Type.Danger
StatusAction {
text: qsTr("This asset")
onTriggered: {
root.showHideRequested(root.currentIndex, false)
communitySubmenu.dismiss()
}
}
StatusAction {
text: qsTr("All assets from this community")
onTriggered: {
root.showHideGroupRequested(root.groupId, false)
communitySubmenu.dismiss()
}
}
}
// token group
StatusAction {
enabled: !root.inHidden && root.isGroup
type: StatusAction.Type.Danger
icon.name: "hide"
text: qsTr("Hide all assets from this community")
onTriggered: root.showHideGroupRequested(root.groupId, false)
}
StatusAction {
enabled: root.inHidden && root.groupId
icon.name: "show"
text: qsTr("Show all assets from this community")
onTriggered: root.showHideGroupRequested(root.groupId, true)
}
}
}
}

View File

@ -0,0 +1,272 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15
import StatusQ.Core 0.1
import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Popups 0.1
import utils 1.0
ComboBox {
id: root
property int sortOrder: Qt.DescendingOrder
readonly property string currentSortRoleName: d.currentSortRoleName
model: d.predefinedSortModel
textRole: "text"
valueRole: "value"
displayText: !d.isCustomSortOrder ? "%1 %2".arg(currentText).arg(sortOrder === Qt.DescendingOrder ? "↓" : "↑")
: currentText
Component.onCompleted: currentIndex = indexOfValue(SortOrderComboBox.TokenOrderCustom)
enum TokenOrder {
TokenOrderNone = 0,
TokenOrderCustom,
TokenOrderValue,
TokenOrderBalance,
TokenOrder1WChange,
TokenOrderAlpha
}
horizontalPadding: 12
verticalPadding: 8
spacing: 8
font.family: Theme.palette.baseFont.name
font.pixelSize: Style.current.additionalTextSize
QtObject {
id: d
readonly property int defaultDelegateHeight: 34
// // models
// readonly property SortFilterProxyModel tokensModel: SortFilterProxyModel {
// sourceModel: root.baseModel
// proxyRoles: [
// ExpressionRole {
// name: "currentBalance"
// expression: model.enabledNetworkBalance.amount
// },
// ExpressionRole {
// name: "currentCurrencyBalance"
// expression: model.enabledNetworkCurrencyBalance.amount
// }
// ]
// sorters: RoleSorter {
// roleName: cmbTokenOrder.currentSortRoleName
// sortOrder: cmbTokenOrder.sortOrder
// enabled: !d.isCustomSortOrder
// }
// filters: ValueFilter {
// roleName: "visibleForNetworkWithPositiveBalance"
// value: true
// }
// }
readonly property var predefinedSortModel: [
{ value: SortOrderComboBox.TokenOrderValue, text: qsTr("Token value"), icon: "token-sale", sortRoleName: "currentCurrencyBalance" }, // custom SFPM ExpressionRole
{ value: SortOrderComboBox.TokenOrderBalance, text: qsTr("Token balance"), icon: "wallet", sortRoleName: "currentBalance" }, // custom SFPM ExpressionRole
{ value: SortOrderComboBox.TokenOrder1WChange, text: qsTr("1W change"), icon: "time", sortRoleName: "changePct24hour" }, // FIXME changePct1Week role missing in backend!!!
{ value: SortOrderComboBox.TokenOrderAlpha, text: qsTr("Alphabetic"), icon: "bold", sortRoleName: "name" },
{ value: SortOrderComboBox.TokenOrderNone, text: "---", icon: "", sortRoleName: "" },
{ value: SortOrderComboBox.TokenOrderCustom, text: qsTr("Custom order"), icon: "exchange", sortRoleName: "" }
]
readonly property string currentSortRoleName: root.currentIndex !== -1 ? d.predefinedSortModel[root.currentIndex].sortRoleName : ""
readonly property bool isCustomSortOrder: root.currentValue === SortOrderComboBox.TokenOrderCustom
}
background: Rectangle {
border.width: 1
border.color: Theme.palette.directColor7
radius: 8
color: root.down ? Theme.palette.baseColor2 : "transparent"
HoverHandler {
cursorShape: root.enabled ? Qt.PointingHandCursor : undefined
}
}
contentItem: StatusBaseText {
leftPadding: root.horizontalPadding
rightPadding: root.horizontalPadding
font.pixelSize: root.font.pixelSize
font.weight: Font.Medium
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
text: root.displayText
color: Theme.palette.baseColor1
}
indicator: StatusIcon {
x: root.mirrored ? root.horizontalPadding : root.width - width - root.horizontalPadding
y: root.topPadding + (root.availableHeight - height) / 2
width: 16
height: width
icon: "chevron-down"
color: Theme.palette.baseColor1
}
popup: Popup {
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
y: root.height + 4
implicitWidth: root.width
margins: 8
padding: 1
verticalPadding: 8
background: Rectangle {
color: Theme.palette.statusSelect.menuItemBackgroundColor
radius: 8
border.color: Theme.palette.baseColor2
layer.enabled: true
layer.effect: DropShadow {
horizontalOffset: 0
verticalOffset: 4
radius: 12
samples: 25
spread: 0.2
color: Theme.palette.dropShadow
}
}
contentItem: ColumnLayout {
StatusBaseText {
Layout.fillWidth: true
Layout.preferredHeight: d.defaultDelegateHeight
text: qsTr("Sort by")
font.pixelSize: Style.current.tertiaryTextFontSize
leftPadding: Style.current.padding
verticalAlignment: Qt.AlignVCenter
color: Theme.palette.baseColor1
}
StatusListView {
Layout.fillWidth: true
implicitWidth: contentWidth
implicitHeight: contentHeight
model: root.popup.visible ? root.delegateModel : null
currentIndex: root.highlightedIndex
}
}
}
Component {
id: regularMenuComponent
RowLayout {
spacing: root.spacing
StatusIcon {
visible: !!icon
icon: iconName
color: root.enabled ? Theme.palette.primaryColor1 : Theme.palette.baseColor1
width: 16
height: 16
}
StatusBaseText {
Layout.fillWidth: true
Layout.fillHeight: true
text: menuText
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
color: root.enabled ? Theme.palette.directColor1 : Theme.palette.baseColor1
font.pixelSize: root.font.pixelSize
font.weight: root.currentIndex === menuIndex ? Font.DemiBold : Font.Normal
}
Item { Layout.fillWidth: true }
Row {
visible: !isCustomOrder
spacing: 4
StatusFlatRoundButton {
radius: 6
width: 24
height: 24
icon.name: "arrow-up"
icon.width: 18
icon.height: 18
opacity: root.highlightedIndex === menuIndex || highlighted // not "visible, we want the item to stay put
highlighted: root.currentIndex === menuIndex && root.sortOrder === Qt.AscendingOrder
onClicked: {
if (root.currentIndex !== menuIndex)
root.currentIndex = menuIndex
root.sortOrder = Qt.AscendingOrder
root.popup.close()
}
}
StatusFlatRoundButton {
radius: 6
width: 24
height: 24
icon.name: "arrow-down"
icon.width: 18
icon.height: 18
opacity: root.highlightedIndex === menuIndex || highlighted // not "visible, we want the item to stay put
highlighted: root.currentIndex === menuIndex && root.sortOrder === Qt.DescendingOrder
onClicked: {
if (root.currentIndex !== menuIndex)
root.currentIndex = menuIndex
root.sortOrder = Qt.DescendingOrder
root.popup.close()
}
}
}
}
}
Component {
id: separatorMenuComponent
StatusMenuSeparator {}
}
delegate: ItemDelegate {
required property int index
required property var modelData
readonly property bool isSeparator: text === "---"
id: menuDelegate
width: root.width
highlighted: root.highlightedIndex === index
enabled: !isSeparator
leftPadding: isSeparator ? 0 : 14
rightPadding: isSeparator ? 0 : 8
verticalPadding: isSeparator ? 2 : 5
spacing: root.spacing
font: root.font
text: root.textRole ? (Array.isArray(root.model) ? modelData[root.textRole] : model[root.textRole])
: modelData
icon.name: modelData["icon"]
icon.color: Theme.palette.primaryColor1
background: Rectangle {
implicitHeight: parent.isSeparator ? 3 : d.defaultDelegateHeight
color: {
if (menuDelegate.index === root.currentIndex)
return Theme.palette.primaryColor3
if (menuDelegate.highlighted)
return Theme.palette.statusMenu.hoverBackgroundColor
return "transparent"
}
HoverHandler {
cursorShape: root.enabled ? Qt.PointingHandCursor : undefined
}
}
contentItem: Loader {
readonly property int menuIndex: menuDelegate.index
readonly property string menuText: menuDelegate.text
readonly property string iconName: menuDelegate.icon.name
readonly property bool isCustomOrder: !menuDelegate.modelData["sortRoleName"]
sourceComponent: menuDelegate.isSeparator ? separatorMenuComponent : regularMenuComponent
}
onClicked: root.currentIndex = index
}
}

View File

@ -4,3 +4,5 @@ AccountHeaderGradient 1.0 AccountHeaderGradient.qml
StatusTxProgressBar 1.0 StatusTxProgressBar.qml
StatusDateRangePicker 1.0 StatusDateRangePicker.qml
ActivityFilterTagItem 1.0 ActivityFilterTagItem.qml
SortOrderComboBox 1.0 SortOrderComboBox.qml
ManageTokenMenuButton 1.0 ManageTokenMenuButton.qml

View File

@ -0,0 +1,378 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ.Core 0.1
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Popups 0.1
import StatusQ.Models 0.1
import utils 1.0
import shared.controls 1.0
import AppLayouts.Wallet.controls 1.0
Control {
id: root
required property var baseModel
readonly property bool dirty: d.controller.dirty
background: null
function saveSettings() {
d.controller.saveSettings();
}
function revert() {
d.controller.revert();
}
function clearSettings() {
d.controller.clearSettings();
}
QtObject {
id: d
property bool communityGroupsExpanded: true
readonly property var controller: ManageTokensController {
sourceModel: root.baseModel
arrangeByCommunity: switchArrangeByCommunity.checked
settingsKey: "WalletAssets"
}
}
component CommunityTag: InformationTag {
tagPrimaryLabel.font.weight: Font.Medium
customBackground: Component {
Rectangle {
color: Theme.palette.baseColor4
radius: 20
}
}
}
component LocalTokenDelegate: DropArea {
id: delegateRoot
property int visualIndex: index
property alias dragEnabled: delegate.dragEnabled
property alias bgColor: delegate.bgColor
property alias topInset: delegate.topInset
property alias bottomInset: delegate.bottomInset
property bool isGrouped
property bool isHidden
property int count
ListView.onRemove: SequentialAnimation {
PropertyAction { target: delegateRoot; property: "ListView.delayRemove"; value: true }
NumberAnimation { target: delegateRoot; property: "scale"; to: 0; easing.type: Easing.InOutQuad }
PropertyAction { target: delegateRoot; property: "ListView.delayRemove"; value: false }
}
width: ListView.view.width
height: visible ? delegate.height : 0
onEntered: function(drag) {
var from = drag.source.visualIndex
var to = delegate.visualIndex
if (to === from)
return
//console.warn("!!! DROP from/to", from, to)
ListView.view.model.moveItem(from, to)
drag.accept()
}
StatusDraggableListItem {
id: delegate
visualIndex: index
dragParent: root
Drag.keys: delegateRoot.keys
draggable: true
width: delegateRoot.width
title: model.name// + " (%1 -> %2)".arg(index).arg(model.customSortOrderNo)
secondaryTitle: hovered || menuBtn.menuVisible ? "%1 <b>·</b> %2".arg(LocaleUtils.currencyAmountToLocaleString(model.enabledNetworkBalance))
.arg(LocaleUtils.currencyAmountToLocaleString(model.enabledNetworkCurrencyBalance))
: LocaleUtils.currencyAmountToLocaleString(model.enabledNetworkBalance)
hasImage: true
icon.source: model.imageUrl || Constants.tokenIcon(model.symbol)
icon.width: 32
icon.height: 32
spacing: 12
actions: [
CommunityTag {
tagPrimaryLabel.text: model.communityName
visible: !!model.communityId && !delegateRoot.isGrouped
image.source: model.communityImage
},
ManageTokenMenuButton {
id: menuBtn
currentIndex: visualIndex
count: delegateRoot.count
inHidden: delegateRoot.isHidden
groupId: model.communityId
isCommunityAsset: !!model.communityId
onMoveRequested: (from, to) => isCommunityAsset ? d.controller.communityTokensModel.moveItem(from, to)
: d.controller.regularTokensModel.moveItem(from, to)
onShowHideRequested: (index, flag) => isCommunityAsset ? d.controller.showHideCommunityToken(index, flag)
: d.controller.showHideRegularToken(index, flag)
onShowHideGroupRequested: (groupId, flag) => d.controller.showHideGroup(groupId, flag)
}
]
}
}
component LocalTokenGroupDelegate: DropArea {
id: communityDelegateRoot
property int visualIndex: index
readonly property string communityId: model.communityId
readonly property int childCount: model.enabledNetworkBalance // NB using "balance" as "count" in m_communityTokenGroupsModel
ListView.onRemove: SequentialAnimation {
PropertyAction { target: communityDelegateRoot; property: "ListView.delayRemove"; value: true }
NumberAnimation { target: communityDelegateRoot; property: "scale"; to: 0; easing.type: Easing.InOutQuad }
PropertyAction { target: communityDelegateRoot; property: "ListView.delayRemove"; value: false }
}
keys: ["x-status-draggable-community-group-item"]
visible: childCount
width: ListView.view.width
height: visible ? groupedCommunityTokenDelegate.implicitHeight : 0
onEntered: function(drag) {
var from = drag.source.visualIndex
var to = groupedCommunityTokenDelegate.visualIndex
if (to === from)
return
//console.warn("!!! DROP GROUP from/to", from, to)
ListView.view.model.moveItem(from, to)
drag.accept()
}
StatusDraggableListItem {
id: groupedCommunityTokenDelegate
width: parent.width
height: dragActive ? implicitHeight : parent.height
leftPadding: Style.current.halfPadding
rightPadding: Style.current.halfPadding
bottomPadding: Style.current.halfPadding
topPadding: 22
draggable: true
spacing: 12
bgColor: Theme.palette.baseColor4
visualIndex: index
dragParent: root
Drag.keys: communityDelegateRoot.keys
contentItem: ColumnLayout {
spacing: 0
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: 12
Layout.rightMargin: 12
Layout.bottomMargin: 14
spacing: groupedCommunityTokenDelegate.spacing
StatusIcon {
Layout.preferredWidth: 20
Layout.preferredHeight: 20
icon: "justify"
color: Theme.palette.baseColor1
}
StatusRoundedImage {
radius: groupedCommunityTokenDelegate.bgRadius
Layout.preferredWidth: 32
Layout.preferredHeight: 32
image.source: model.communityImage
showLoadingIndicator: true
image.fillMode: Image.PreserveAspectCrop
}
StatusBaseText {
text: model.communityName// + "(%1 -> %2)".arg(index).arg(model.customSortOrderNo)
elide: Text.ElideRight
maximumLineCount: 1
font.weight: Font.Medium
}
StatusBaseText {
Layout.leftMargin: -parent.spacing/2
text: "<b>·</b> %1".arg(qsTr("%n asset(s)", "", communityDelegateRoot.childCount))
elide: Text.ElideRight
color: Theme.palette.baseColor1
maximumLineCount: 1
visible: !d.communityGroupsExpanded
}
Item { Layout.fillWidth: true }
ManageTokenMenuButton {
currentIndex: visualIndex
count: d.controller.communityTokenGroupsModel.count
isGroup: true
groupId: model.communityId
onMoveRequested: (from, to) => d.controller.communityTokenGroupsModel.moveItem(from, to)
onShowHideGroupRequested: (groupId, flag) => d.controller.showHideGroup(groupId, flag)
}
}
StatusListView {
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
model: d.controller.communityTokensModel
interactive: false
visible: d.communityGroupsExpanded
displaced: Transition {
NumberAnimation { properties: "x,y"; easing.type: Easing.OutQuad }
}
delegate: LocalTokenDelegate {
isGrouped: true
count: communityDelegateRoot.childCount
dragEnabled: count > 1
keys: ["x-status-draggable-community-token-item-%1".arg(model.communityId)]
bgColor: Theme.palette.indirectColor4
topInset: 2 // tighter "spacing"
bottomInset: 2
visible: communityDelegateRoot.communityId === model.communityId
}
}
}
}
}
contentItem: ColumnLayout {
spacing: Style.current.padding
StatusListView {
Layout.fillWidth: true
model: d.controller.regularTokensModel
implicitHeight: contentHeight
interactive: false
displaced: Transition {
NumberAnimation { properties: "x,y"; easing.type: Easing.OutQuad }
}
delegate: LocalTokenDelegate {
count: d.controller.regularTokensModel.count
dragEnabled: count > 1
keys: ["x-status-draggable-token-item"]
}
}
RowLayout {
id: communityTokensHeader
Layout.fillWidth: true
Layout.topMargin: Style.current.padding
visible: d.controller.communityTokensModel.count
StatusBaseText {
color: Theme.palette.baseColor1
text: qsTr("Community")// + " -> %1".arg(switchArrangeByCommunity.checked ? d.controller.communityTokenGroupsModel.count : d.controller.communityTokensModel.count)
}
Item { Layout.fillWidth: true }
StatusSwitch {
LayoutMirroring.enabled: true
LayoutMirroring.childrenInherit: true
id: switchArrangeByCommunity
textColor: Theme.palette.baseColor1
text: qsTr("Arrange by community")
}
}
StatusModalDivider {
Layout.fillWidth: true
Layout.topMargin: -Style.current.halfPadding
visible: communityTokensHeader.visible && switchArrangeByCommunity.checked
}
StatusLinkText {
Layout.alignment: Qt.AlignTrailing
visible: communityTokensHeader.visible && switchArrangeByCommunity.checked
text: d.communityGroupsExpanded ? qsTr("Collapse all") : qsTr("Expand all")
normalColor: linkColor
font.weight: Font.Normal
onClicked: d.communityGroupsExpanded = !d.communityGroupsExpanded
}
Loader {
Layout.fillWidth: true
active: d.controller.communityTokensModel.count
visible: active
sourceComponent: switchArrangeByCommunity.checked ? cmpCommunityTokenGroups : cmpCommunityTokens
}
StatusBaseText {
Layout.fillWidth: true
Layout.topMargin: Style.current.padding
color: Theme.palette.baseColor1
text: qsTr("Hidden")// + " -> %1".arg(d.controller.hiddenTokensModel.count)
visible: d.controller.hiddenTokensModel.count
}
StatusListView {
Layout.fillWidth: true
model: d.controller.hiddenTokensModel
implicitHeight: contentHeight
interactive: false
displaced: Transition {
NumberAnimation { properties: "x,y"; easing.type: Easing.OutQuad }
}
delegate: LocalTokenDelegate {
dragEnabled: false
keys: ["x-status-draggable-none"]
isHidden: true
}
}
}
Component {
id: cmpCommunityTokens
StatusListView {
model: d.controller.communityTokensModel
implicitHeight: contentHeight
interactive: false
displaced: Transition {
NumberAnimation { properties: "x,y"; easing.type: Easing.OutQuad }
}
delegate: LocalTokenDelegate {
count: d.controller.communityTokensModel.count
dragEnabled: count > 1
keys: ["x-status-draggable-community-token-item"]
}
}
}
Component {
id: cmpCommunityTokenGroups
StatusListView {
model: d.controller.communityTokenGroupsModel
implicitHeight: contentHeight
interactive: false
spacing: Style.current.halfPadding
displaced: Transition {
NumberAnimation { properties: "x,y"; easing.type: Easing.OutQuad }
}
delegate: LocalTokenGroupDelegate {}
}
}
}

View File

@ -2,3 +2,4 @@ WalletHeader 1.0 WalletHeader.qml
WalletTxProgressBlock 1.0 WalletTxProgressBlock.qml
WalletNftPreview 1.0 WalletNftPreview.qml
ActivityFilterPanel 1.0 ActivityFilterPanel.qml
ManageTokensPanel 1.0 ManageTokensPanel.qml

View File

@ -11,8 +11,8 @@ import utils 1.0
Control {
id: root
property alias image : image
property alias iconAsset : iconAsset
property alias image: image
property alias iconAsset: iconAsset
property alias tagPrimaryLabel: tagPrimaryLabel
property alias tagSecondaryLabel: tagSecondaryLabel
property alias middleLabel: middleLabel
@ -31,63 +31,54 @@ Control {
QtObject {
id: d
property var loadingComponent: Component { LoadingComponent {}}
property var loadingComponent: Component { LoadingComponent {} }
}
horizontalPadding: Style.current.halfPadding
verticalPadding: 5
horizontalPadding: 12
verticalPadding: 8
spacing: 4
background: Loader {
sourceComponent: root.loading ? d.loadingComponent : root.customBackground
}
contentItem: RowLayout {
spacing: 4
spacing: root.spacing
visible: !root.loading
// FIXME this could be StatusIcon but it can't load images from an arbitrary URL
Image {
id: image
Layout.alignment: Qt.AlignVCenter
Layout.maximumWidth: visible ? 16 : 0
Layout.maximumHeight: visible ? 16 : 0
visible: image.source !== ""
visible: !!source
}
StatusIcon {
id: iconAsset
Layout.alignment: Qt.AlignVCenter
Layout.maximumWidth: visible ? 16 : 0
Layout.maximumHeight: visible ? 16 : 0
visible: iconAsset.icon !== ""
visible: !!icon
}
StatusBaseText {
id: tagPrimaryLabel
Layout.alignment: Qt.AlignVCenter
font.pixelSize: Style.current.tertiaryTextFontSize
font.weight: Font.Normal
color: Theme.palette.directColor1
visible: text !== ""
}
StatusBaseText {
id: middleLabel
Layout.alignment: Qt.AlignVCenter
font.pixelSize: Style.current.tertiaryTextFontSize
font.weight: Font.Normal
color: Theme.palette.baseColor1
visible: text !== ""
}
StatusBaseText {
id: tagSecondaryLabel
Layout.alignment: Qt.AlignVCenter
Layout.maximumWidth: root.secondarylabelMaxWidth
font.pixelSize: Style.current.tertiaryTextFontSize
font.weight: Font.Normal
color: Theme.palette.baseColor1
visible: text !== ""
elide: Text.ElideMiddle
}
Loader {
id: rightComponent
Layout.alignment: Qt.AlignVCenter
}
}
}