2022-09-13 14:25:16 +02:00
|
|
|
/****************************************************************************
|
|
|
|
|
**
|
|
|
|
|
** Copyright (C) 2022 The Qt Company Ltd.
|
|
|
|
|
** Contact: https://www.qt.io/licensing/
|
|
|
|
|
**
|
|
|
|
|
** This file is part of Qt Creator.
|
|
|
|
|
**
|
|
|
|
|
** Commercial License Usage
|
|
|
|
|
** Licensees holding valid commercial Qt licenses may use this file in
|
|
|
|
|
** accordance with the commercial license agreement provided with the
|
|
|
|
|
** Software or, alternatively, in accordance with the terms contained in
|
|
|
|
|
** a written agreement between you and The Qt Company. For licensing terms
|
|
|
|
|
** and conditions see https://www.qt.io/terms-conditions. For further
|
|
|
|
|
** information use the contact form at https://www.qt.io/contact-us.
|
|
|
|
|
**
|
|
|
|
|
** GNU General Public License Usage
|
|
|
|
|
** Alternatively, this file may be used under the terms of the GNU
|
|
|
|
|
** General Public License version 3 as published by the Free Software
|
|
|
|
|
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
|
|
|
|
** included in the packaging of this file. Please review the following
|
|
|
|
|
** information to ensure the GNU General Public License requirements will
|
|
|
|
|
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
|
|
|
|
**
|
|
|
|
|
****************************************************************************/
|
|
|
|
|
|
|
|
|
|
import QtQuick
|
|
|
|
|
import QtQuick.Controls
|
|
|
|
|
import StatesEditor
|
2022-09-22 22:23:52 +02:00
|
|
|
import HelperWidgets 2.0 as HelperWidgets
|
2022-09-13 14:25:16 +02:00
|
|
|
import StudioControls 1.0 as StudioControls
|
|
|
|
|
import StudioTheme as StudioTheme
|
|
|
|
|
|
|
|
|
|
Rectangle {
|
|
|
|
|
id: root
|
|
|
|
|
|
|
|
|
|
signal createNewState
|
|
|
|
|
signal cloneState(int internalNodeId)
|
|
|
|
|
signal extendState(int internalNodeId)
|
|
|
|
|
signal deleteState(int internalNodeId)
|
|
|
|
|
|
|
|
|
|
property bool isLandscape: true
|
|
|
|
|
|
|
|
|
|
color: StudioTheme.Values.themeStatePanelBackground
|
|
|
|
|
|
|
|
|
|
onWidthChanged: root.responsiveResize(root.width, root.height)
|
|
|
|
|
onHeightChanged: root.responsiveResize(root.width, root.height)
|
|
|
|
|
|
|
|
|
|
Component.onCompleted: root.responsiveResize(root.width, root.height)
|
|
|
|
|
|
|
|
|
|
function numFit(overall, size, space) {
|
|
|
|
|
let tmpNum = Math.floor(overall / size)
|
|
|
|
|
let spaceLeft = overall - (tmpNum * size)
|
|
|
|
|
return spaceLeft - (space * (tmpNum - 1)) >= 0 ? tmpNum : tmpNum - 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function responsiveResize(width, height) {
|
|
|
|
|
height -= toolBar.height + (2 * root.padding)
|
|
|
|
|
width -= (2 * root.padding)
|
|
|
|
|
|
|
|
|
|
var numStates = statesRepeater.count - 1 // Subtract base state
|
|
|
|
|
var numRows = 0
|
|
|
|
|
var numColumns = 0
|
|
|
|
|
|
|
|
|
|
// Size extension in case of extend groups are shown
|
|
|
|
|
var sizeExtension = root.showExtendGroups ? root.extend : 0
|
|
|
|
|
var doubleSizeExtension = root.showExtendGroups ? 2 * root.extend : 0
|
|
|
|
|
|
|
|
|
|
// Get view orientation (LANDSCAPE, PORTRAIT)
|
|
|
|
|
if (width >= height) {
|
|
|
|
|
root.isLandscape = true
|
|
|
|
|
outerGrid.columns = 3
|
|
|
|
|
outerGrid.rows = 1
|
|
|
|
|
// Three outer section height (base state, middle, plus button)
|
|
|
|
|
baseStateWrapper.height = height
|
|
|
|
|
root.scrollViewHeight = height
|
|
|
|
|
addWrapper.height = height
|
|
|
|
|
|
|
|
|
|
height -= doubleSizeExtension
|
|
|
|
|
|
|
|
|
|
if (height > Constants.maxThumbSize) {
|
|
|
|
|
// In this case we want to have a multi row grid in the center
|
|
|
|
|
root.thumbSize = Constants.maxThumbSize
|
|
|
|
|
|
|
|
|
|
let tmpScrollViewWidth = width - root.thumbSize * 1.5 - 2 * root.outerGridSpacing
|
|
|
|
|
|
|
|
|
|
// Inner grid calculation
|
|
|
|
|
numRows = root.numFit(height, Constants.maxThumbSize, root.innerGridSpacing)
|
|
|
|
|
numColumns = Math.min(numStates, root.numFit(tmpScrollViewWidth, root.thumbSize,
|
|
|
|
|
root.innerGridSpacing))
|
|
|
|
|
|
|
|
|
|
let tmpRows = Math.ceil(numStates / numColumns)
|
|
|
|
|
|
|
|
|
|
if (tmpRows <= numRows)
|
|
|
|
|
numRows = tmpRows
|
|
|
|
|
else
|
|
|
|
|
numColumns = Math.ceil(numStates / numRows)
|
|
|
|
|
} else {
|
|
|
|
|
// This case is for single row layout and small thumb view
|
|
|
|
|
root.thumbSize = Math.max(height, Constants.minThumbSize)
|
|
|
|
|
|
|
|
|
|
// Inner grid calculation
|
|
|
|
|
numColumns = numStates
|
|
|
|
|
numRows = 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Constants.thumbnailSize = root.thumbSize
|
|
|
|
|
|
|
|
|
|
let tmpWidth = root.thumbSize * numColumns + root.innerGridSpacing * (numColumns - 1) + doubleSizeExtension
|
|
|
|
|
let remainingSpace = width - root.thumbSize - 2 * root.outerGridSpacing
|
|
|
|
|
let space = remainingSpace - tmpWidth
|
|
|
|
|
|
|
|
|
|
if (space >= root.thumbSize) {
|
|
|
|
|
root.scrollViewWidth = tmpWidth
|
|
|
|
|
addWrapper.width = space
|
|
|
|
|
} else {
|
|
|
|
|
addWrapper.width = Math.max(space, 0.5 * root.thumbSize)
|
|
|
|
|
root.scrollViewWidth = remainingSpace - addWrapper.width
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
root.topMargin = (root.scrollViewHeight - (root.thumbSize * numRows)
|
|
|
|
|
- root.innerGridSpacing * (numRows - 1)) * 0.5 - sizeExtension
|
|
|
|
|
|
|
|
|
|
addCanvas.width = Math.min(addWrapper.width, root.thumbSize)
|
|
|
|
|
addCanvas.height = root.thumbSize
|
|
|
|
|
|
|
|
|
|
baseStateWrapper.width = root.thumbSize
|
|
|
|
|
|
|
|
|
|
baseStateThumbnail.anchors.verticalCenter = baseStateWrapper.verticalCenter
|
|
|
|
|
baseStateThumbnail.anchors.horizontalCenter = undefined
|
|
|
|
|
|
|
|
|
|
addCanvas.anchors.verticalCenter = addWrapper.verticalCenter
|
|
|
|
|
addCanvas.anchors.horizontalCenter = undefined
|
|
|
|
|
addCanvas.anchors.top = undefined
|
|
|
|
|
addCanvas.anchors.left = addWrapper.left
|
|
|
|
|
|
|
|
|
|
root.leftMargin = 0 // resetting left margin in case of orientation switch
|
|
|
|
|
} else {
|
|
|
|
|
root.isLandscape = false
|
|
|
|
|
outerGrid.rows = 3
|
|
|
|
|
outerGrid.columns = 1
|
|
|
|
|
// Three outer section width (base state, middle, plus button)
|
|
|
|
|
baseStateWrapper.width = width
|
|
|
|
|
root.scrollViewWidth = width
|
|
|
|
|
addWrapper.width = width
|
|
|
|
|
|
|
|
|
|
width -= doubleSizeExtension
|
|
|
|
|
|
|
|
|
|
if (width > Constants.maxThumbSize) {
|
|
|
|
|
// In this case we want to have a multi column grid in the center
|
|
|
|
|
root.thumbSize = Constants.maxThumbSize
|
|
|
|
|
|
|
|
|
|
let tmpScrollViewHeight = height - root.thumbSize * 1.5 - 2 * root.outerGridSpacing
|
|
|
|
|
|
|
|
|
|
// Inner grid calculation
|
|
|
|
|
numRows = Math.min(numStates, root.numFit(tmpScrollViewHeight, root.thumbSize,
|
|
|
|
|
root.innerGridSpacing))
|
|
|
|
|
numColumns = root.numFit(width, Constants.maxThumbSize, root.innerGridSpacing)
|
|
|
|
|
|
|
|
|
|
let tmpColumns = Math.ceil(numStates / numRows)
|
|
|
|
|
|
|
|
|
|
if (tmpColumns <= numColumns)
|
|
|
|
|
numColumns = tmpColumns
|
|
|
|
|
else
|
|
|
|
|
numRows = Math.ceil(numStates / numColumns)
|
|
|
|
|
} else {
|
|
|
|
|
// This case is for single column layout and small thumb view
|
|
|
|
|
root.thumbSize = Math.max(width, Constants.minThumbSize)
|
|
|
|
|
|
|
|
|
|
// Inner grid calculation
|
|
|
|
|
numRows = numStates
|
|
|
|
|
numColumns = 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Constants.thumbnailSize = root.thumbSize
|
|
|
|
|
|
|
|
|
|
let tmpHeight = root.thumbSize * numRows + root.innerGridSpacing * (numRows - 1) + doubleSizeExtension
|
|
|
|
|
let remainingSpace = height - root.thumbSize - 2 * root.outerGridSpacing
|
|
|
|
|
let space = remainingSpace - tmpHeight
|
|
|
|
|
|
|
|
|
|
if (space >= root.thumbSize) {
|
|
|
|
|
root.scrollViewHeight = tmpHeight
|
|
|
|
|
addWrapper.height = space
|
|
|
|
|
} else {
|
|
|
|
|
addWrapper.height = Math.max(space, 0.5 * root.thumbSize)
|
|
|
|
|
root.scrollViewHeight = remainingSpace - addWrapper.height
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
root.leftMargin = (root.scrollViewWidth - (root.thumbSize * numColumns)
|
|
|
|
|
- root.innerGridSpacing * (numColumns - 1)) * 0.5 - sizeExtension
|
|
|
|
|
|
|
|
|
|
addCanvas.width = root.thumbSize
|
|
|
|
|
addCanvas.height = Math.min(addWrapper.height, root.thumbSize)
|
|
|
|
|
|
|
|
|
|
baseStateWrapper.height = root.thumbSize
|
|
|
|
|
|
|
|
|
|
baseStateThumbnail.anchors.verticalCenter = undefined
|
|
|
|
|
baseStateThumbnail.anchors.horizontalCenter = baseStateWrapper.horizontalCenter
|
|
|
|
|
|
|
|
|
|
addCanvas.anchors.verticalCenter = undefined
|
|
|
|
|
addCanvas.anchors.horizontalCenter = addWrapper.horizontalCenter
|
|
|
|
|
addCanvas.anchors.top = addWrapper.top
|
|
|
|
|
addCanvas.anchors.left = undefined
|
|
|
|
|
|
|
|
|
|
root.topMargin = 0 // resetting top margin in case of orientation switch
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Always assign the bigger one first otherwise there will be console output complaining
|
|
|
|
|
if (numRows > innerGrid.rows) {
|
|
|
|
|
innerGrid.rows = numRows
|
|
|
|
|
innerGrid.columns = numColumns
|
|
|
|
|
} else {
|
|
|
|
|
innerGrid.columns = numColumns
|
|
|
|
|
innerGrid.rows = numRows
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// These function assume that the order of the states is as follows:
|
|
|
|
|
// State A, State B (extends State A), ... so the extended state always comes first
|
|
|
|
|
function isInRange(i) {
|
|
|
|
|
return i >= 0 && i < statesEditorModel.count()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function nextStateHasExtend(i) {
|
|
|
|
|
let next = i + 1
|
|
|
|
|
return root.isInRange(next) ? statesEditorModel.get(next).hasExtend : false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function previousStateHasExtend(i) {
|
|
|
|
|
let prev = i - 1
|
|
|
|
|
return root.isInRange(prev) ? statesEditorModel.get(prev).hasExtend : false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
property bool showExtendGroups: statesEditorModel.hasExtend
|
|
|
|
|
|
|
|
|
|
onShowExtendGroupsChanged: root.responsiveResize(root.width, root.height)
|
|
|
|
|
|
|
|
|
|
property int extend: 16
|
|
|
|
|
|
|
|
|
|
property int thumbSize: 250
|
|
|
|
|
|
|
|
|
|
property int padding: 10
|
|
|
|
|
|
|
|
|
|
property int scrollViewWidth: 640
|
|
|
|
|
property int scrollViewHeight: 480
|
|
|
|
|
property int outerGridSpacing: 10
|
|
|
|
|
property int innerGridSpacing: root.showExtendGroups ? 40 : root.outerGridSpacing
|
|
|
|
|
|
|
|
|
|
// These margins are used to push the inner grid down or to the left depending on the views
|
|
|
|
|
// orientation to align to the outer grid
|
|
|
|
|
property int topMargin: 0
|
|
|
|
|
property int leftMargin: 0
|
|
|
|
|
|
|
|
|
|
property bool tinyMode: Constants.thumbnailSize <= Constants.thumbnailBreak
|
|
|
|
|
|
|
|
|
|
property int currentStateInternalId: 0
|
2022-10-06 11:15:22 +02:00
|
|
|
// Using an int instead of a bool, because when opening a menu on one state and without closing
|
|
|
|
|
// opening a menu on another state will first trigger the open of the new popup and afterwards
|
|
|
|
|
// the close of the old popup. Using an int keeps track of number of opened popups.
|
|
|
|
|
property int menuOpen: 0
|
2022-09-13 14:25:16 +02:00
|
|
|
|
|
|
|
|
// This timer is used to delay the current state animation as it didn't work due to the
|
|
|
|
|
// repeaters item not being positioned in time resulting in 0 x and y position if the grids
|
|
|
|
|
// row and column were not changed during the layout algorithm .
|
|
|
|
|
Timer {
|
|
|
|
|
id: layoutTimer
|
|
|
|
|
interval: 50
|
|
|
|
|
running: false
|
|
|
|
|
repeat: false
|
|
|
|
|
onTriggered: {
|
|
|
|
|
// Move the current state into view if outside
|
|
|
|
|
if (root.currentStateInternalId === 0)
|
|
|
|
|
// Not for base state
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
var x = 0
|
|
|
|
|
var y = 0
|
|
|
|
|
for (var i = 0; i < statesRepeater.count; ++i) {
|
|
|
|
|
let item = statesRepeater.itemAt(i)
|
|
|
|
|
|
|
|
|
|
if (item.internalNodeId === root.currentStateInternalId) {
|
|
|
|
|
x = item.x
|
|
|
|
|
y = item.y
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if it is in view
|
|
|
|
|
if (x <= frame.contentX
|
|
|
|
|
|| x >= (frame.contentX + root.scrollViewWidth - root.thumbSize))
|
|
|
|
|
frame.contentX = x - root.scrollViewWidth * 0.5 + root.thumbSize * 0.5
|
|
|
|
|
|
|
|
|
|
if (y <= frame.contentY
|
|
|
|
|
|| y >= (frame.contentY + root.scrollViewHeight - root.thumbSize))
|
|
|
|
|
frame.contentY = y - root.scrollViewHeight * 0.5 + root.thumbSize * 0.5
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onCurrentStateInternalIdChanged: layoutTimer.start()
|
|
|
|
|
|
|
|
|
|
StudioControls.Dialog {
|
|
|
|
|
id: editDialog
|
|
|
|
|
title: qsTr("Rename state group")
|
|
|
|
|
standardButtons: Dialog.Apply | Dialog.Cancel
|
|
|
|
|
x: editButton.x - Math.max(0, editButton.x + editDialog.width - root.width)
|
|
|
|
|
y: toolBar.height
|
|
|
|
|
closePolicy: Popup.NoAutoClose
|
|
|
|
|
|
|
|
|
|
width: Math.min(300, root.width)
|
|
|
|
|
|
2022-09-27 17:52:52 +02:00
|
|
|
function apply() {
|
2022-09-13 14:25:16 +02:00
|
|
|
let renamed = statesEditorModel.renameActiveStateGroup(editTextField.text)
|
|
|
|
|
if (renamed)
|
|
|
|
|
editDialog.close()
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-27 17:52:52 +02:00
|
|
|
onApplied: editDialog.accept()
|
|
|
|
|
|
2022-09-13 14:25:16 +02:00
|
|
|
StudioControls.TextField {
|
|
|
|
|
id: editTextField
|
|
|
|
|
actionIndicatorVisible: false
|
|
|
|
|
translationIndicatorVisible: false
|
|
|
|
|
anchors.fill: parent
|
2022-09-27 17:52:52 +02:00
|
|
|
|
|
|
|
|
onTextChanged: {
|
|
|
|
|
let btn = editDialog.standardButton(Dialog.Apply)
|
|
|
|
|
if (!btn)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if (editDialog.previousString !== editTextField.text) {
|
|
|
|
|
btn.enabled = true
|
|
|
|
|
} else {
|
|
|
|
|
btn.enabled = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onAccepted: editDialog.accept()
|
|
|
|
|
onRejected: editDialog.reject()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onAccepted: {
|
|
|
|
|
let renamed = statesEditorModel.renameActiveStateGroup(editTextField.text)
|
|
|
|
|
if (renamed)
|
|
|
|
|
editDialog.close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
property string previousString
|
|
|
|
|
|
|
|
|
|
onAboutToShow: {
|
|
|
|
|
editTextField.text = statesEditorModel.activeStateGroup
|
|
|
|
|
editDialog.previousString = statesEditorModel.activeStateGroup
|
|
|
|
|
|
|
|
|
|
let btn = editDialog.standardButton(Dialog.Apply)
|
|
|
|
|
btn.enabled = false
|
2022-09-13 14:25:16 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Rectangle {
|
|
|
|
|
id: toolBar
|
|
|
|
|
|
|
|
|
|
property bool doubleRow: root.width < 450
|
|
|
|
|
|
|
|
|
|
onDoubleRowChanged: {
|
|
|
|
|
if (toolBar.doubleRow) {
|
|
|
|
|
toolBarGrid.rows = 2
|
|
|
|
|
toolBarGrid.columns = 1
|
|
|
|
|
} else {
|
|
|
|
|
toolBarGrid.columns = 2
|
|
|
|
|
toolBarGrid.rows = 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
color: StudioTheme.Values.themeSectionHeadBackground
|
|
|
|
|
width: root.width
|
|
|
|
|
height: (toolBar.doubleRow ? 2 : 1) * StudioTheme.Values.toolbarHeight
|
|
|
|
|
|
|
|
|
|
Grid {
|
|
|
|
|
id: toolBarGrid
|
|
|
|
|
columns: 2
|
|
|
|
|
rows: 1
|
|
|
|
|
columnSpacing: StudioTheme.Values.toolbarSpacing
|
|
|
|
|
|
|
|
|
|
Row {
|
|
|
|
|
id: stateGroupSelectionRow
|
|
|
|
|
height: StudioTheme.Values.toolbarHeight
|
|
|
|
|
spacing: StudioTheme.Values.toolbarSpacing
|
|
|
|
|
leftPadding: root.padding
|
|
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
id: stateGroupLabel
|
|
|
|
|
color: StudioTheme.Values.themeTextColor
|
|
|
|
|
text: qsTr("State Group")
|
|
|
|
|
font.pixelSize: StudioTheme.Values.baseFontSize
|
|
|
|
|
horizontalAlignment: Text.AlignRight
|
|
|
|
|
verticalAlignment: Text.AlignVCenter
|
|
|
|
|
height: StudioTheme.Values.height
|
|
|
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
|
visible: root.width > 240
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
StudioControls.ComboBox {
|
|
|
|
|
id: stateGroupComboBox
|
|
|
|
|
actionIndicatorVisible: false
|
|
|
|
|
model: statesEditorModel.stateGroups
|
|
|
|
|
currentIndex: statesEditorModel.activeStateGroupIndex
|
|
|
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
|
width: stateGroupLabel.visible ? StudioTheme.Values.defaultControlWidth
|
|
|
|
|
: root.width - 2 * root.padding
|
|
|
|
|
|
2022-09-22 22:23:52 +02:00
|
|
|
HelperWidgets.Tooltip { id: comboBoxTooltip }
|
|
|
|
|
|
|
|
|
|
Timer {
|
|
|
|
|
interval: 1000
|
|
|
|
|
running: stateGroupComboBox.hovered
|
|
|
|
|
onTriggered: comboBoxTooltip.showText(stateGroupComboBox,
|
|
|
|
|
hoverHandler.point.position,
|
|
|
|
|
qsTr("Switch State Group"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onHoverChanged: {
|
|
|
|
|
if (!stateGroupComboBox.hovered)
|
|
|
|
|
comboBoxTooltip.hideText()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
HoverHandler { id: hoverHandler }
|
|
|
|
|
|
2022-09-13 14:25:16 +02:00
|
|
|
popup.onOpened: editDialog.close()
|
|
|
|
|
|
|
|
|
|
// currentIndex needs special treatment, because if model is changed, it will be
|
|
|
|
|
// reset regardless of binding.
|
|
|
|
|
Connections {
|
|
|
|
|
target: statesEditorModel
|
|
|
|
|
function onActiveStateGroupIndexChanged() {
|
|
|
|
|
stateGroupComboBox.currentIndex = statesEditorModel.activeStateGroupIndex
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onModelChanged: {
|
|
|
|
|
stateGroupComboBox.currentIndex = statesEditorModel.activeStateGroupIndex
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onCompressedActivated: function (index, reason) {
|
|
|
|
|
statesEditorModel.activeStateGroupIndex = index
|
|
|
|
|
root.responsiveResize(root.width, root.height)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Row {
|
|
|
|
|
Row {
|
|
|
|
|
id: stateGroupEditRow
|
|
|
|
|
height: StudioTheme.Values.toolbarHeight
|
|
|
|
|
spacing: StudioTheme.Values.toolbarSpacing
|
|
|
|
|
leftPadding: toolBar.doubleRow ? root.padding : 0
|
|
|
|
|
|
2022-09-22 22:23:52 +02:00
|
|
|
HelperWidgets.AbstractButton {
|
2022-09-13 14:25:16 +02:00
|
|
|
buttonIcon: StudioTheme.Constants.plus
|
|
|
|
|
anchors.verticalCenter: parent.verticalCenter
|
2022-09-22 22:23:52 +02:00
|
|
|
tooltip: qsTr("Create State Group")
|
2022-09-13 14:25:16 +02:00
|
|
|
onClicked: statesEditorModel.addStateGroup("stateGroup")
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-22 22:23:52 +02:00
|
|
|
HelperWidgets.AbstractButton {
|
2022-09-13 14:25:16 +02:00
|
|
|
buttonIcon: StudioTheme.Constants.minus
|
|
|
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
|
enabled: statesEditorModel.activeStateGroupIndex !== 0
|
2022-09-22 22:23:52 +02:00
|
|
|
tooltip: qsTr("Remove State Group")
|
2022-09-13 14:25:16 +02:00
|
|
|
onClicked: statesEditorModel.removeStateGroup()
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-22 22:23:52 +02:00
|
|
|
HelperWidgets.AbstractButton {
|
2022-09-13 14:25:16 +02:00
|
|
|
id: editButton
|
|
|
|
|
buttonIcon: StudioTheme.Constants.edit
|
|
|
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
|
enabled: statesEditorModel.activeStateGroupIndex !== 0
|
|
|
|
|
checked: editDialog.visible
|
2022-09-22 22:23:52 +02:00
|
|
|
tooltip: qsTr("Rename State Group")
|
2022-09-13 14:25:16 +02:00
|
|
|
onClicked: {
|
|
|
|
|
if (editDialog.opened)
|
|
|
|
|
editDialog.close()
|
|
|
|
|
else
|
|
|
|
|
editDialog.open()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Item {
|
|
|
|
|
width: Math.max(0, toolBar.width - (toolBar.doubleRow ? 0 : (stateGroupSelectionRow.width
|
|
|
|
|
+ toolBarGrid.columnSpacing))
|
|
|
|
|
- stateGroupEditRow.width - thumbnailToggleRow.width)
|
|
|
|
|
height: 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Row {
|
|
|
|
|
id: thumbnailToggleRow
|
|
|
|
|
height: StudioTheme.Values.toolbarHeight
|
|
|
|
|
spacing: StudioTheme.Values.toolbarSpacing
|
|
|
|
|
rightPadding: root.padding
|
|
|
|
|
|
2022-09-22 22:23:52 +02:00
|
|
|
HelperWidgets.AbstractButton {
|
2022-09-13 14:25:16 +02:00
|
|
|
buttonIcon: StudioTheme.Constants.gridView
|
|
|
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
|
enabled: !root.tinyMode
|
2022-09-22 22:23:52 +02:00
|
|
|
tooltip: qsTr("Show thumbnails")
|
2022-09-13 14:25:16 +02:00
|
|
|
onClicked: {
|
|
|
|
|
for (var i = 0; i < statesRepeater.count; ++i)
|
|
|
|
|
statesRepeater.itemAt(i).setPropertyChangesVisible(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-22 22:23:52 +02:00
|
|
|
HelperWidgets.AbstractButton {
|
2022-09-13 14:25:16 +02:00
|
|
|
buttonIcon: StudioTheme.Constants.textFullJustification
|
|
|
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
|
enabled: !root.tinyMode
|
2022-09-22 22:23:52 +02:00
|
|
|
tooltip: qsTr("Show property changes")
|
2022-09-13 14:25:16 +02:00
|
|
|
onClicked: {
|
|
|
|
|
for (var i = 0; i < statesRepeater.count; ++i)
|
|
|
|
|
statesRepeater.itemAt(i).setPropertyChangesVisible(true)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Grid {
|
|
|
|
|
id: outerGrid
|
|
|
|
|
x: root.padding
|
|
|
|
|
y: toolBar.height + root.padding
|
|
|
|
|
columns: 3
|
|
|
|
|
rows: 1
|
|
|
|
|
spacing: root.outerGridSpacing
|
|
|
|
|
|
|
|
|
|
Item {
|
|
|
|
|
id: baseStateWrapper
|
|
|
|
|
|
|
|
|
|
StateThumbnail {
|
|
|
|
|
// Base State
|
|
|
|
|
id: baseStateThumbnail
|
|
|
|
|
width: Constants.thumbnailSize
|
|
|
|
|
height: Constants.thumbnailSize
|
|
|
|
|
baseState: true
|
|
|
|
|
defaultChecked: !statesEditorModel.baseState.modelHasDefaultState // TODO Make this one a model property
|
|
|
|
|
isChecked: root.currentStateInternalId === 0
|
|
|
|
|
thumbnailImageSource: statesEditorModel.baseState.stateImageSource // TODO Get rid of the QVariantMap
|
|
|
|
|
isTiny: root.tinyMode
|
|
|
|
|
|
|
|
|
|
onFocusSignal: root.currentStateInternalId = 0
|
|
|
|
|
onDefaultClicked: statesEditorModel.resetDefaultState()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Item {
|
|
|
|
|
id: scrollViewWrapper
|
|
|
|
|
width: root.isLandscape ? root.scrollViewWidth : root.width - (2 * root.padding)
|
|
|
|
|
height: root.isLandscape ? root.height - toolBar.height - (2 * root.padding) : root.scrollViewHeight
|
|
|
|
|
clip: true
|
|
|
|
|
|
|
|
|
|
ScrollView {
|
|
|
|
|
id: scrollView
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
anchors.topMargin: root.topMargin
|
|
|
|
|
anchors.leftMargin: root.leftMargin
|
|
|
|
|
|
|
|
|
|
ScrollBar.horizontal: StateScrollBar {
|
2022-09-26 11:36:14 +02:00
|
|
|
id: horizontalBar
|
2022-09-13 14:25:16 +02:00
|
|
|
parent: scrollView
|
|
|
|
|
x: scrollView.leftPadding
|
|
|
|
|
y: scrollView.height - height
|
|
|
|
|
width: scrollView.availableWidth
|
|
|
|
|
orientation: Qt.Horizontal
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ScrollBar.vertical: StateScrollBar {
|
2022-09-26 11:36:14 +02:00
|
|
|
id: verticalBar
|
2022-09-13 14:25:16 +02:00
|
|
|
parent: scrollView
|
|
|
|
|
x: scrollView.mirrored ? 0 : scrollView.width - width
|
|
|
|
|
y: scrollView.topPadding
|
|
|
|
|
height: scrollView.availableHeight
|
|
|
|
|
orientation: Qt.Vertical
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Flickable {
|
|
|
|
|
id: frame
|
|
|
|
|
boundsMovement: Flickable.StopAtBounds
|
|
|
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
|
|
|
interactive: true
|
|
|
|
|
contentWidth: {
|
|
|
|
|
let ext = root.showExtendGroups ? (2 * root.extend) : 0
|
|
|
|
|
return innerGrid.width + ext
|
|
|
|
|
}
|
|
|
|
|
contentHeight: {
|
|
|
|
|
let ext = root.showExtendGroups ? (2 * root.extend) : 0
|
|
|
|
|
return innerGrid.height + ext
|
|
|
|
|
}
|
2022-09-20 11:46:17 +02:00
|
|
|
flickableDirection: {
|
|
|
|
|
if (frame.contentHeight <= scrollView.height)
|
|
|
|
|
return Flickable.HorizontalFlick
|
|
|
|
|
|
|
|
|
|
if (frame.contentWidth <= scrollView.width)
|
|
|
|
|
return Flickable.VerticalFlick
|
|
|
|
|
|
|
|
|
|
return Flickable.HorizontalAndVerticalFlick
|
|
|
|
|
}
|
2022-09-13 14:25:16 +02:00
|
|
|
|
|
|
|
|
Behavior on contentY {
|
|
|
|
|
NumberAnimation {
|
|
|
|
|
duration: 1000
|
|
|
|
|
easing.type: Easing.InOutCubic
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Behavior on contentX {
|
|
|
|
|
NumberAnimation {
|
|
|
|
|
duration: 1000
|
|
|
|
|
easing.type: Easing.InOutCubic
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Grid {
|
|
|
|
|
id: innerGrid
|
|
|
|
|
|
|
|
|
|
x: root.showExtendGroups ? root.extend : 0
|
|
|
|
|
y: root.showExtendGroups ? root.extend : 0
|
|
|
|
|
|
|
|
|
|
rows: 1
|
|
|
|
|
spacing: root.innerGridSpacing
|
|
|
|
|
|
|
|
|
|
move: Transition {
|
|
|
|
|
NumberAnimation {
|
|
|
|
|
properties: "x,y"
|
|
|
|
|
easing.type: Easing.OutQuad
|
2022-09-26 11:37:28 +02:00
|
|
|
duration: 100
|
2022-09-13 14:25:16 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Repeater {
|
|
|
|
|
id: statesRepeater
|
|
|
|
|
|
|
|
|
|
property int grabIndex: -1
|
|
|
|
|
|
|
|
|
|
model: statesEditorModel
|
|
|
|
|
|
|
|
|
|
onItemAdded: root.responsiveResize(root.width, root.height)
|
|
|
|
|
onItemRemoved: root.responsiveResize(root.width, root.height)
|
|
|
|
|
|
|
|
|
|
delegate: DropArea {
|
|
|
|
|
id: delegateRoot
|
|
|
|
|
|
|
|
|
|
required property int index
|
|
|
|
|
|
|
|
|
|
required property string stateName
|
|
|
|
|
required property var stateImageSource
|
|
|
|
|
required property int internalNodeId
|
|
|
|
|
required property var hasWhenCondition
|
|
|
|
|
required property var whenConditionString
|
|
|
|
|
required property bool isDefault
|
|
|
|
|
required property var modelHasDefaultState
|
|
|
|
|
required property bool hasExtend
|
|
|
|
|
required property var extendString
|
|
|
|
|
|
|
|
|
|
function setPropertyChangesVisible(value) {
|
2022-09-29 12:59:07 +02:00
|
|
|
stateThumbnail.setPropertyChangesVisible(value)
|
2022-09-13 14:25:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
width: Constants.thumbnailSize
|
|
|
|
|
height: Constants.thumbnailSize
|
|
|
|
|
|
|
|
|
|
visible: delegateRoot.internalNodeId // Skip base state
|
|
|
|
|
|
|
|
|
|
property int visualIndex: index
|
|
|
|
|
|
|
|
|
|
onEntered: function (drag) {
|
|
|
|
|
let dragSource = (drag.source as StateThumbnail)
|
|
|
|
|
|
|
|
|
|
if (dragSource === undefined)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if (dragSource.extendString !== stateThumbnail.extendString
|
|
|
|
|
|| stateThumbnail.extendedState) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-23 23:53:27 +02:00
|
|
|
statesEditorModel.move(dragSource.visualIndex,
|
|
|
|
|
stateThumbnail.visualIndex)
|
2022-09-13 14:25:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onDropped: function (drop) {
|
2022-09-23 23:53:27 +02:00
|
|
|
let dropSource = (drop.source as StateThumbnail)
|
2022-09-13 14:25:16 +02:00
|
|
|
|
2022-09-23 23:53:27 +02:00
|
|
|
if (dropSource === undefined)
|
2022-09-13 14:25:16 +02:00
|
|
|
return
|
|
|
|
|
|
2022-09-23 23:53:27 +02:00
|
|
|
if (dropSource.extendString !== stateThumbnail.extendString
|
2022-09-13 14:25:16 +02:00
|
|
|
|| stateThumbnail.extendedState) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-23 23:53:27 +02:00
|
|
|
if (statesRepeater.grabIndex === dropSource.visualIndex)
|
2022-09-21 18:07:23 +02:00
|
|
|
return
|
|
|
|
|
|
2022-09-23 23:53:27 +02:00
|
|
|
statesEditorModel.drop(statesRepeater.grabIndex,
|
|
|
|
|
dropSource.visualIndex)
|
|
|
|
|
statesRepeater.grabIndex = -1
|
2022-09-13 14:25:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extend Groups Visualization
|
|
|
|
|
Rectangle {
|
|
|
|
|
id: extendBackground
|
|
|
|
|
x: -root.extend
|
|
|
|
|
y: -root.extend
|
|
|
|
|
width: Constants.thumbnailSize + 2 * root.extend
|
|
|
|
|
height: Constants.thumbnailSize + 2 * root.extend
|
|
|
|
|
color: StudioTheme.Values.themeStateHighlight
|
|
|
|
|
|
|
|
|
|
radius: {
|
|
|
|
|
if (root.nextStateHasExtend(delegateRoot.index))
|
|
|
|
|
return delegateRoot.hasExtend ? 0 : root.extend
|
|
|
|
|
|
|
|
|
|
return root.extend
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
visible: (delegateRoot.hasExtend
|
|
|
|
|
|| stateThumbnail.extendedState)
|
|
|
|
|
}
|
|
|
|
|
// Fill the gap between extend group states and also cover up radius
|
|
|
|
|
// of start and end states of an extend group in case of line break
|
|
|
|
|
Rectangle {
|
|
|
|
|
id: extendGap
|
|
|
|
|
property bool portraitOneColumn: !root.isLandscape
|
|
|
|
|
&& innerGrid.columns === 1
|
|
|
|
|
|
|
|
|
|
property bool leftOrTop: {
|
|
|
|
|
if (delegateRoot.hasExtend)
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
if (root.previousStateHasExtend(delegateRoot.index))
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
property bool rightOrBottom: {
|
|
|
|
|
if (stateThumbnail.extendedState)
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
if (root.nextStateHasExtend(delegateRoot.index))
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
property bool firstInRow: ((delegateRoot.index - 1) % innerGrid.columns) === 0
|
|
|
|
|
property bool lastInRow: ((delegateRoot.index - 1) % innerGrid.columns)
|
|
|
|
|
=== (innerGrid.columns - 1)
|
|
|
|
|
|
|
|
|
|
x: {
|
|
|
|
|
if (!extendGap.portraitOneColumn) {
|
|
|
|
|
if (extendGap.rightOrBottom)
|
|
|
|
|
return extendGap.lastInRow ? Constants.thumbnailSize
|
|
|
|
|
- (root.innerGridSpacing
|
|
|
|
|
- root.extend) : Constants.thumbnailSize
|
|
|
|
|
if (extendGap.leftOrTop)
|
|
|
|
|
return extendGap.firstInRow ? -root.extend : -root.innerGridSpacing
|
|
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return -root.extend
|
|
|
|
|
}
|
|
|
|
|
y: {
|
|
|
|
|
if (extendGap.portraitOneColumn) {
|
|
|
|
|
if (extendGap.rightOrBottom)
|
|
|
|
|
return Constants.thumbnailSize
|
|
|
|
|
if (extendGap.leftOrTop)
|
|
|
|
|
return -root.innerGridSpacing
|
|
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return -root.extend
|
|
|
|
|
}
|
|
|
|
|
width: extendGap.portraitOneColumn ? Constants.thumbnailSize + 2
|
|
|
|
|
* root.extend : root.innerGridSpacing
|
|
|
|
|
height: extendGap.portraitOneColumn ? root.innerGridSpacing : Constants.thumbnailSize
|
|
|
|
|
+ 2 * root.extend
|
|
|
|
|
color: StudioTheme.Values.themeStateHighlight
|
2022-09-26 16:29:26 +02:00
|
|
|
visible: extendBackground.visible
|
2022-09-13 14:25:16 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
StateThumbnail {
|
|
|
|
|
id: stateThumbnail
|
|
|
|
|
width: Constants.thumbnailSize
|
|
|
|
|
height: Constants.thumbnailSize
|
|
|
|
|
visualIndex: delegateRoot.visualIndex
|
|
|
|
|
internalNodeId: delegateRoot.internalNodeId
|
|
|
|
|
isTiny: root.tinyMode
|
|
|
|
|
|
|
|
|
|
hasExtend: delegateRoot.hasExtend
|
|
|
|
|
extendString: delegateRoot.extendString
|
|
|
|
|
extendedState: statesEditorModel.extendedStates.includes(
|
|
|
|
|
delegateRoot.stateName)
|
|
|
|
|
|
|
|
|
|
hasWhenCondition: delegateRoot.hasWhenCondition
|
|
|
|
|
|
2022-10-06 11:15:22 +02:00
|
|
|
blockDragHandler: horizontalBar.active || verticalBar.active
|
|
|
|
|
|| root.menuOpen
|
2022-09-26 11:36:14 +02:00
|
|
|
|
2022-09-23 23:53:27 +02:00
|
|
|
dragParent: scrollView
|
|
|
|
|
|
2022-10-06 11:15:22 +02:00
|
|
|
onMenuOpenChanged: {
|
|
|
|
|
if (stateThumbnail.menuOpen)
|
|
|
|
|
root.menuOpen++
|
|
|
|
|
else
|
|
|
|
|
root.menuOpen--
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-13 14:25:16 +02:00
|
|
|
// Fix ScrollView taking over the dragging event
|
|
|
|
|
onGrabbing: {
|
|
|
|
|
frame.interactive = false
|
|
|
|
|
statesRepeater.grabIndex = stateThumbnail.visualIndex
|
|
|
|
|
}
|
|
|
|
|
onLetGo: frame.interactive = true
|
|
|
|
|
|
|
|
|
|
stateName: delegateRoot.stateName
|
|
|
|
|
thumbnailImageSource: delegateRoot.stateImageSource
|
|
|
|
|
whenCondition: delegateRoot.whenConditionString
|
|
|
|
|
|
|
|
|
|
baseState: !delegateRoot.internalNodeId
|
|
|
|
|
defaultChecked: delegateRoot.isDefault
|
|
|
|
|
isChecked: root.currentStateInternalId === delegateRoot.internalNodeId
|
|
|
|
|
|
|
|
|
|
onFocusSignal: root.currentStateInternalId = delegateRoot.internalNodeId
|
|
|
|
|
onDefaultClicked: statesEditorModel.setStateAsDefault(
|
|
|
|
|
delegateRoot.internalNodeId)
|
|
|
|
|
|
|
|
|
|
onClone: root.cloneState(delegateRoot.internalNodeId)
|
|
|
|
|
onExtend: root.extendState(delegateRoot.internalNodeId)
|
2022-09-22 11:17:02 +02:00
|
|
|
onRemove: {
|
|
|
|
|
if (delegateRoot.isDefault)
|
|
|
|
|
statesEditorModel.resetDefaultState()
|
|
|
|
|
|
|
|
|
|
root.deleteState(delegateRoot.internalNodeId)
|
|
|
|
|
}
|
2022-09-13 14:25:16 +02:00
|
|
|
|
|
|
|
|
onStateNameFinished: statesEditorModel.renameState(
|
|
|
|
|
delegateRoot.internalNodeId,
|
|
|
|
|
stateThumbnail.stateName)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Item {
|
|
|
|
|
id: addWrapper
|
|
|
|
|
|
|
|
|
|
Canvas {
|
|
|
|
|
id: addCanvas
|
|
|
|
|
width: root.thumbWidth
|
|
|
|
|
height: root.thumbHeight
|
|
|
|
|
|
|
|
|
|
onPaint: {
|
|
|
|
|
var ctx = getContext("2d")
|
|
|
|
|
|
|
|
|
|
ctx.strokeStyle = StudioTheme.Values.themeStateHighlight
|
|
|
|
|
ctx.lineWidth = 6
|
|
|
|
|
|
|
|
|
|
var plusExtend = 20
|
|
|
|
|
var halfWidth = addCanvas.width / 2
|
|
|
|
|
var halfHeight = addCanvas.height / 2
|
|
|
|
|
|
|
|
|
|
ctx.beginPath()
|
|
|
|
|
ctx.moveTo(halfWidth, halfHeight - plusExtend)
|
|
|
|
|
ctx.lineTo(halfWidth, halfHeight + plusExtend)
|
|
|
|
|
|
|
|
|
|
ctx.moveTo(halfWidth - plusExtend, halfHeight)
|
|
|
|
|
ctx.lineTo(halfWidth + plusExtend, halfHeight)
|
|
|
|
|
ctx.stroke()
|
|
|
|
|
|
|
|
|
|
ctx.save()
|
|
|
|
|
ctx.setLineDash([2, 2])
|
|
|
|
|
ctx.strokeRect(0, 0, addCanvas.width, addCanvas.height)
|
|
|
|
|
ctx.restore()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MouseArea {
|
|
|
|
|
id: addMouseArea
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
hoverEnabled: true
|
|
|
|
|
onClicked: root.createNewState()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Rectangle {
|
|
|
|
|
// temporary hover indicator for add button
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
opacity: 0.1
|
|
|
|
|
color: addMouseArea.containsMouse ? "#ffffff" : "#000000"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|