Merge "Merge remote-tracking branch 'origin/qds/dev'"

This commit is contained in:
The Qt Project
2025-03-13 12:53:39 +00:00
203 changed files with 7248 additions and 5652 deletions

View File

@@ -196,9 +196,7 @@ function(qtc_enable_sanitize _target _sanitize_flags)
endfunction()
function(qtc_deeper_concept_diagnostic_depth _target)
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_compile_options("${_target}" PUBLIC -fconcepts-diagnostics-depth=8)
endif()
target_compile_options("${_target}" PRIVATE $<$<COMPILE_LANG_AND_ID:CXX,GNU>:-fconcepts-diagnostics-depth=8>)
endfunction()
function(qtc_add_link_flags_no_undefined target)

View File

@@ -55,7 +55,6 @@ set(DESIGNSTUDIO_PLUGINS
Texteditor
UpdateInfo
VcsBase
assetexporterplugin
componentsplugin
qmlpreviewplugin
qtquickplugin)

View File

@@ -141,6 +141,10 @@
\externalpage https://doc.qt.io/qtdesignstudio/qtquick-positioning.html#using-layouts
\title Using layouts
*/
/*!
\externalpage https://www.qt.io/blog/qt-design-studio-4.7-released
\title Qt Design Studio 4.7 released
*/
/*!
\externalpage https://www.qt.io/blog/qt-design-studio-4.6-released
\title Qt Design Studio 4.6 released

View File

@@ -14,8 +14,6 @@
to \QDS and to edit them to create a UI.
The following sections describe some of the main points of the webinar.
Select the \uicontrol Tutorials tab to watch the webinar video for the
full details.
\section1 Exporting from Adobe Photoshop

View File

@@ -16,7 +16,7 @@
\list
\li In the \uicontrol 3D or \uicontrol Navigator view, right-click a 3D component and select
\uicontrol {Export Component}.
\uicontrol {Export Bundle}.
\li In the \uicontrol {Material Browser} view, right-click a material and select
\uicontrol {Export Material}.
\endlist
@@ -25,7 +25,7 @@
To import a 3D component or material bundle, do one of the following:
\list
\li In the \uicontrol {3D}, \uicontrol {2D}, or \uicontrol {Navigator} view, right-click
and select \uicontrol {Import Component}. If you use this method to import a bundle of 3D
and select \uicontrol {Import Bundle}. If you use this method to import a bundle of 3D
components, the 3D components are added to the 3D scene.
\li In \uicontrol {Content Library} > \uicontrol {User Assets}, right-click and select
\uicontrol {Import Bundle}. If you use this method to import a bundle of 3D

View File

@@ -9,11 +9,12 @@
Read the \QDS release blog posts to see what's new in every version.
\section1 \QDS Release blog posts
\section1 \QDS release blog posts
\section2 \QDS 4
\list
\li \l{Qt Design Studio 4.7 released}
\li \l{Qt Design Studio 4.6 released}
\li \l{Qt Design Studio 4.5.1 released}
\li \l{Qt Design Studio 4.5 released}

View File

@@ -141,6 +141,15 @@ StudioControls.Menu {
onTriggered: root.rootView.editAssetComponent(root.__selectedAssetPathsList[0])
}
StudioControls.MenuItem {
id: updateComponent
text: qsTr("Reimport 3D Asset")
visible: root.__fileIndex && root.__selectedAssetPathsList.length === 1
&& root.rootView.assetIsImported3d(root.__selectedAssetPathsList[0])
height: editComponent.visible ? editComponent.implicitHeight : 0
onTriggered: root.rootView.updateAssetComponent(root.__selectedAssetPathsList[0])
}
StudioControls.MenuItem {
id: addTexturesItem
text: qsTr("Add Texture")

View File

@@ -5,6 +5,7 @@ import QtQuick
import QtQuick.Controls
import StudioControls as StudioControls
import StudioTheme as StudioTheme
import HelperWidgets as HelperWidgets
Column {
id: root
@@ -21,13 +22,13 @@ Column {
Row {
spacing: root.horizontalSpacing
PopupLabel {
HelperWidgets.PopupLabel {
width: root.columnWidth
text: qsTr("From")
tooltip: qsTr("Sets the component and its property from which the value is copied.")
}
PopupLabel {
HelperWidgets.PopupLabel {
width: root.columnWidth
text: qsTr("To")
tooltip: qsTr("Sets the property of the selected component to which the copied value is assigned.")
@@ -49,7 +50,7 @@ Column {
onCurrentTypeIndexChanged: sourceNode.currentIndex = sourceNode.currentTypeIndex
}
PopupLabel {
HelperWidgets.PopupLabel {
width: root.columnWidth
text: backend.targetNode
anchors.verticalCenter: parent.verticalCenter

View File

@@ -49,8 +49,6 @@ StudioControls.PopupDialog {
ConnectionsDialogForm {
id: form
parentWindow: root.window
Connections {
target: root.backend
function onPopupShouldClose() {

View File

@@ -6,6 +6,8 @@ import QtQuick.Controls
import HelperWidgets as HelperWidgets
import StudioControls as StudioControls
import StudioTheme as StudioTheme
import ScriptsEditor as ScriptsEditor
import ScriptEditorBackend
Column {
id: root
@@ -16,8 +18,7 @@ Column {
property var backend
property bool keepOpen: expressionDialogLoader.visible
property Window parentWindow: null
property bool keepOpen: scriptEditor.keepOpen
width: parent.width
spacing: root.verticalSpacing
@@ -29,13 +30,13 @@ Column {
Row {
spacing: root.horizontalSpacing
PopupLabel {
HelperWidgets.PopupLabel {
width: root.columnWidth
text: qsTr("Signal")
tooltip: qsTr("Sets an interaction method that connects to the <b>Target</b> component.")
}
PopupLabel {
HelperWidgets.PopupLabel {
width: root.columnWidth
text: qsTr("Action")
tooltip: qsTr("Sets an action that is associated with the selected <b>Target</b> component's <b>Signal</b>.")
@@ -58,259 +59,40 @@ Column {
onCurrentTypeIndexChanged: signal.currentIndex = signal.currentTypeIndex
}
StudioControls.TopLevelComboBox {
ScriptsEditor.ActionsComboBox {
id: action
style: StudioTheme.Values.connectionPopupControlStyle
width: root.columnWidth
textRole: "text"
valueRole: "value"
///model.getData(currentIndex, "role")
property int indexFromBackend: indexOfValue(backend.actionType)
onIndexFromBackendChanged: action.currentIndex = action.indexFromBackend
onActivated: backend.changeActionType(action.currentValue)
model: ListModel {
ListElement {
value: ConnectionModelStatementDelegate.CallFunction
text: qsTr("Call Function")
enabled: true
}
ListElement {
value: ConnectionModelStatementDelegate.Assign
text: qsTr("Assign")
enabled: true
}
ListElement {
value: ConnectionModelStatementDelegate.ChangeState
text: qsTr("Change State")
enabled: true
}
ListElement {
value: ConnectionModelStatementDelegate.SetProperty
text: qsTr("Set Property")
enabled: true
}
ListElement {
value: ConnectionModelStatementDelegate.PrintMessage
text: qsTr("Print Message")
enabled: true
}
ListElement {
value: ConnectionModelStatementDelegate.Custom
text: qsTr("Custom")
enabled: false
}
}
backend: root.backend
}
}
StatementEditor {
width: root.width
actionType: action.currentValue ?? ConnectionModelStatementDelegate.Custom
ScriptsEditor.ScriptEditorForm {
id: scriptEditor
anchors.left: parent.left
anchors.right: parent.right
horizontalSpacing: root.horizontalSpacing
verticalSpacing: root.verticalSpacing
columnWidth: root.columnWidth
statement: backend.okStatement
spacing: root.spacing
backend: root.backend
spacing: root.verticalSpacing
}
HelperWidgets.AbstractButton {
style: StudioTheme.Values.connectionPopupButtonStyle
width: 160
buttonIcon: qsTr("Add Condition")
tooltip: qsTr("Sets a logical condition for the selected <b>Signal</b>. It works with the properties of the <b>Target</b> component.")
iconSize: StudioTheme.Values.baseFontSize
iconFontFamily: StudioTheme.Constants.font.family
anchors.horizontalCenter: parent.horizontalCenter
visible: action.currentValue !== ConnectionModelStatementDelegate.Custom && !backend.hasCondition
currentAction: action.currentValue ?? StatementDelegate.Custom
onClicked: backend.addCondition()
}
HelperWidgets.AbstractButton {
style: StudioTheme.Values.connectionPopupButtonStyle
width: 160
buttonIcon: qsTr("Remove Condition")
tooltip: qsTr("Removes the logical condition for the <b>Target</b> component.")
iconSize: StudioTheme.Values.baseFontSize
iconFontFamily: StudioTheme.Constants.font.family
anchors.horizontalCenter: parent.horizontalCenter
visible: action.currentValue !== ConnectionModelStatementDelegate.Custom && backend.hasCondition
onClicked: backend.removeCondition()
}
ExpressionBuilder {
style: StudioTheme.Values.connectionPopupControlStyle
width: root.width
visible: backend.hasCondition
model: backend.conditionListModel
onRemove: function(index) {
//console.log("remove", index)
backend.conditionListModel.removeToken(index)
}
onUpdate: function(index, value) {
//console.log("update", index, value)
backend.conditionListModel.updateToken(index, value)
}
onAdd: function(value) {
//console.log("add", value)
backend.conditionListModel.appendToken(value)
}
onInsert: function(index, value, type) {
//console.log("insert", index, value, type)
if (type === ConditionListModel.Intermediate)
backend.conditionListModel.insertIntermediateToken(index, value)
else if (type === ConditionListModel.Shadow)
backend.conditionListModel.insertShadowToken(index, value)
else
backend.conditionListModel.insertToken(index, value)
}
onSetValue: function(index, value) {
//console.log("setValue", index, value)
backend.conditionListModel.setShadowToken(index, value)
}
}
HelperWidgets.AbstractButton {
style: StudioTheme.Values.connectionPopupButtonStyle
width: 160
buttonIcon: qsTr("Add Else Statement")
tooltip: qsTr("Sets an alternate condition for the previously defined logical condition.")
iconSize: StudioTheme.Values.baseFontSize
iconFontFamily: StudioTheme.Constants.font.family
anchors.horizontalCenter: parent.horizontalCenter
visible: action.currentValue !== ConnectionModelStatementDelegate.Custom
&& backend.hasCondition && !backend.hasElse
onClicked: backend.addElse()
}
HelperWidgets.AbstractButton {
style: StudioTheme.Values.connectionPopupButtonStyle
width: 160
buttonIcon: qsTr("Remove Else Statement")
tooltip: qsTr("Removes the alternate logical condition for the previously defined logical condition.")
iconSize: StudioTheme.Values.baseFontSize
iconFontFamily: StudioTheme.Constants.font.family
anchors.horizontalCenter: parent.horizontalCenter
visible: action.currentValue !== ConnectionModelStatementDelegate.Custom
&& backend.hasCondition && backend.hasElse
onClicked: backend.removeElse()
}
//Else Statement
StatementEditor {
width: root.width
actionType: action.currentValue ?? ConnectionModelStatementDelegate.Custom
horizontalSpacing: root.horizontalSpacing
columnWidth: root.columnWidth
statement: backend.koStatement
backend: root.backend
spacing: root.verticalSpacing
visible: action.currentValue !== ConnectionModelStatementDelegate.Custom
&& backend.hasCondition && backend.hasElse
}
// code preview toolbar
Column {
id: miniToolbarEditor
width: parent.width
spacing: -2
Rectangle {
id: miniToolbar
width: parent.width
height: editorButton.height + 2
radius: 4
z: -1
color: StudioTheme.Values.themeConnectionEditorMicroToolbar
Row {
spacing: 2
HelperWidgets.AbstractButton {
id: editorButton
style: StudioTheme.Values.microToolbarButtonStyle
buttonIcon: StudioTheme.Constants.codeEditor_medium
tooltip: qsTr("Write the conditions for the components and the signals manually.")
onClicked: expressionDialogLoader.show()
}
HelperWidgets.AbstractButton {
id: jumpToCodeButton
style: StudioTheme.Values.microToolbarButtonStyle
buttonIcon: StudioTheme.Constants.jumpToCode_medium
tooltip: qsTr("Jump to the code.")
onClicked: backend.jumpToCode()
}
}
}
// Editor
Rectangle {
id: editor
width: parent.width
height: 150
color: StudioTheme.Values.themeConnectionCodeEditor
Text {
id: code
anchors.fill: parent
anchors.margins: 4
text: backend.indentedSource
color: StudioTheme.Values.themeTextColor
font.pixelSize: StudioTheme.Values.myFontSize
wrapMode: Text.Wrap
horizontalAlignment: code.lineCount === 1 ? Text.AlignHCenter : Text.AlignLeft
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
Loader {
id: expressionDialogLoader
parent: editor
anchors.fill: parent
visible: false
active: visible
function show() {
expressionDialogLoader.visible = true
}
sourceComponent: Item {
id: bindingEditorParent
Component.onCompleted: {
bindingEditor.showWidget()
bindingEditor.text = backend.source
bindingEditor.showControls(false)
bindingEditor.setMultilne(true)
bindingEditor.updateWindowName()
}
ActionEditor {
id: bindingEditor
onRejected: {
bindingEditor.hideWidget()
expressionDialogLoader.visible = false
}
onAccepted: {
backend.setNewSource(bindingEditor.text)
bindingEditor.hideWidget()
expressionDialogLoader.visible = false
}
}
}
}
}
itemTooltip: qsTr("Sets the component that is affected by the action of the <b>Target</b> component's <b>Signal</b>.")
methodTooltip: qsTr("Sets the item component's method that is affected by the <b>Target</b> component's <b>Signal</b>.")
fromTooltip: qsTr("Sets the component and its property from which the value is copied when the <b>Target</b> component initiates the <b>Signal</b>.")
toTooltip: qsTr("Sets the component and its property to which the copied value is assigned when the <b>Target</b> component initiates the <b>Signal</b>.")
addConditionTooltip: qsTr("Sets a logical condition for the selected <b>Signal</b>. It works with the properties of the <b>Target</b> component.")
removeConditionTooltip: qsTr("Removes the logical condition for the <b>Target</b> component.")
stateGroupTooltip: qsTr("Sets a <b>State Group</b> that is accessed when the <b>Target</b> component initiates the <b>Signal</b>.")
stateTooltip: qsTr("Sets a <b>State</b> within the assigned <b>State Group</b> that is accessed when the <b>Target</b> component initiates the <b>Signal</b>.")
propertyTooltip: qsTr("Sets the property of the component that is affected by the action of the <b>Target</b> component's <b>Signal</b>.")
valueTooltip: qsTr("Sets the value of the property of the component that is affected by the action of the <b>Target</b> component's <b>Signal</b>.")
messageTooltip: qsTr("Sets a text that is printed when the <b>Signal</b> of the <b>Target</b> component initiates.")
}
}

View File

@@ -5,6 +5,7 @@ import QtQuick
import QtQuick.Controls
import StudioControls as StudioControls
import StudioTheme as StudioTheme
import HelperWidgets as HelperWidgets
Column {
id: root
@@ -18,7 +19,7 @@ Column {
width: parent.width
spacing: root.verticalSpacing
PopupLabel {
HelperWidgets.PopupLabel {
text: qsTr("Type")
tooltip: qsTr("Sets the category of the <b>Local Custom Property</b>.")
}
@@ -37,13 +38,13 @@ Column {
Row {
spacing: root.horizontalSpacing
PopupLabel {
HelperWidgets.PopupLabel {
width: root.columnWidth
text: qsTr("Name")
tooltip: qsTr("Sets a name for the <b>Local Custom Property</b>.")
}
PopupLabel {
HelperWidgets.PopupLabel {
width: root.columnWidth
text: qsTr("Value")
tooltip: qsTr("Sets a valid <b>Local Custom Property</b> value.")

View File

@@ -109,29 +109,35 @@ HelperWidgets.ScrollView {
id: infoText
text: {
if (!ContentLibraryBackend.rootView.isQt6Project)
if (!ContentLibraryBackend.rootView.isQt6Project) {
qsTr("<b>Content Library</b> effects are not supported in Qt5 projects.")
else if (!ContentLibraryBackend.rootView.hasQuick3DImport)
qsTr('To use <b>Content Library</b>, first <a href="#add_import" style="text-decoration:none;color:%1">
add the QtQuick3D module</a> in the <b>Components</b> view.')
.arg(StudioTheme.Values.themeInteraction)
else if (!ContentLibraryBackend.effectsModel.hasRequiredQuick3DImport)
} else if (!ContentLibraryBackend.rootView.hasQuick3DImport) {
qsTr('To use <b>Content Library</b> effects, add the <b>QtQuick3D</b> module and the <b>View3D</b>
component in the <b>Components</b> view, or click
<a href=\"#add_import\"><span style=\"text-decoration:none;color:%1\">
here</span></a>.').arg(StudioTheme.Values.themeInteraction)
} else if (!ContentLibraryBackend.effectsModel.hasRequiredQuick3DImport) {
qsTr("To use <b>Content Library</b>, version 6.4 or later of the QtQuick3D module is required.")
else if (!ContentLibraryBackend.rootView.hasMaterialLibrary)
} else if (!ContentLibraryBackend.rootView.hasMaterialLibrary) {
qsTr("<b>Content Library</b> is disabled inside a non-visual component.")
else if (!ContentLibraryBackend.effectsModel.bundleExists)
} else if (!ContentLibraryBackend.effectsModel.bundleExists) {
qsTr("No effects available.")
else if (!searchBox.isEmpty())
} else if (!searchBox.isEmpty()) {
qsTr("No match found.")
else
} else {
""
}
}
textFormat: Text.RichText
color: StudioTheme.Values.themeTextColor
font.pixelSize: StudioTheme.Values.baseFontSize
topPadding: 10
leftPadding: 10
rightPadding: 10
visible: ContentLibraryBackend.effectsModel.isEmpty
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
width: root.width
onLinkActivated: ContentLibraryBackend.rootView.addQtQuick3D()
}

View File

@@ -114,30 +114,34 @@ HelperWidgets.ScrollView {
id: infoText
text: {
if (!ContentLibraryBackend.rootView.isQt6Project)
if (!ContentLibraryBackend.rootView.isQt6Project) {
qsTr("<b>Content Library</b> materials are not supported in Qt5 projects.")
else if (!ContentLibraryBackend.rootView.hasQuick3DImport)
qsTr('To use <b>Content Library</b>, first <a href="#add_import" style="text-decoration:none;color:%1">
add the QtQuick3D module</a> in the <b>Components</b> view.')
.arg(StudioTheme.Values.themeInteraction)
else if (!root.materialsModel.hasRequiredQuick3DImport)
} else if (!ContentLibraryBackend.rootView.hasQuick3DImport) {
qsTr('To use <b>Content Library</b> materials, add the <b>QtQuick3D</b> module and the <b>View3D</b>
component in the <b>Components</b> view, or click
<a href=\"#add_import\"><span style=\"text-decoration:none;color:%1\">
here</span></a>.').arg(StudioTheme.Values.themeInteraction)
} else if (!root.materialsModel.hasRequiredQuick3DImport) {
qsTr("To use <b>Content Library</b>, version 6.3 or later of the QtQuick3D module is required.")
else if (!ContentLibraryBackend.rootView.hasMaterialLibrary)
} else if (!ContentLibraryBackend.rootView.hasMaterialLibrary) {
qsTr("<b>Content Library</b> is disabled inside a non-visual component.")
else if (!root.materialsModel.bundleExists)
} else if (!root.materialsModel.bundleExists) {
qsTr("No materials available. Make sure you have an internet connection.")
else if (!searchBox.isEmpty())
} else if (!searchBox.isEmpty()) {
qsTr("No match found.")
else
} else {
""
}
}
textFormat: Text.RichText
color: StudioTheme.Values.themeTextColor
font.pixelSize: StudioTheme.Values.baseFontSize
topPadding: 10
leftPadding: 10
rightPadding: 10
wrapMode: Text.WordWrap
width: root.width - x
horizontalAlignment: Text.AlignHCenter
onLinkActivated: ContentLibraryBackend.rootView.addQtQuick3D()
}

View File

@@ -213,26 +213,33 @@ Item {
text: {
let categoryName = (categoryTitle === "3D") ? categoryTitle + " assets"
: categoryTitle.toLowerCase()
if (!ContentLibraryBackend.rootView.isQt6Project)
if (!ContentLibraryBackend.rootView.isQt6Project) {
qsTr("<b>Content Library</b> is not supported in Qt5 projects.")
else if (!ContentLibraryBackend.rootView.hasQuick3DImport && categoryTitle !== "Textures")
qsTr(`To use %1, first <a href="#add_import" style="text-decoration:none;color:%2">
add the <b>QtQuick3D</b> module</a> in the <b>Components</b> view.`)
.arg(categoryName)
.arg(StudioTheme.Values.themeInteraction)
else if (!ContentLibraryBackend.rootView.hasMaterialLibrary && categoryTitle !== "Textures")
} else if (!ContentLibraryBackend.rootView.hasQuick3DImport && categoryTitle !== "Textures") {
qsTr('To use %1, add the <b>QtQuick3D</b> module and the <b>View3D</b>
component in the <b>Components</b> view, or click
<a href=\"#add_import\"><span style=\"text-decoration:none;color:%2\">
here</span></a>.')
.arg(categoryName)
.arg(StudioTheme.Values.themeInteraction)
} else if (!ContentLibraryBackend.rootView.hasMaterialLibrary && categoryTitle !== "Textures") {
qsTr("<b>Content Library</b> is disabled inside a non-visual component.")
else if (categoryEmpty)
} else if (categoryEmpty) {
qsTr("There are no "+ categoryName + " in the <b>User Assets</b>.")
else
} else {
""
}
}
textFormat: Text.RichText
color: StudioTheme.Values.themeTextColor
font.pixelSize: StudioTheme.Values.baseFontSize
topPadding: 10
leftPadding: 10
rightPadding: 10
visible: infoText.text !== ""
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
width: root.width
onLinkActivated: ContentLibraryBackend.rootView.addQtQuick3D()
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import QtQuick.Templates as T
import StudioTheme as StudioTheme
Item {
id: control
property StudioTheme.ControlStyle style: StudioTheme.Values.controlStyle
property alias icon: icon
property bool hover: mouseArea.containsMouse//false
property bool pressed: false
property bool forceVisible: false
implicitWidth: control.style.actionIndicatorSize.width
implicitHeight: control.style.actionIndicatorSize.height
signal clicked
z: 10
T.Label {
id: icon
anchors.fill: parent
text: StudioTheme.Constants.actionIcon
color: control.style.icon.idle
font.family: StudioTheme.Constants.iconFont.family
font.pixelSize: control.style.baseIconFontSize
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
states: [
State {
name: "hover"
when: control.hover && !control.pressed && control.enabled
PropertyChanges {
target: icon
scale: 1.2
visible: true
}
},
State {
name: "disable"
when: !control.enabled
PropertyChanges {
target: icon
color: control.style.icon.disabled
}
}
]
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
//onContainsMouseChanged: control.hover = mouseArea.containsMouse
onClicked: control.clicked()
}
}

View File

@@ -0,0 +1,365 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import QtQuick.Templates as T
import StudioTheme as StudioTheme
import StudioQuickUtils
T.SpinBox {
id: control
property StudioTheme.ControlStyle style: StudioTheme.Values.controlStyle
property real realFrom: 0.0
property real realTo: 99.0
property real realValue: 1.0
property real realStepSize: 1.0
property alias labelColor: spinBoxInput.color
property int decimals: 0
property real minStepSize: {
var tmpMinStepSize = Number((control.realStepSize * 0.1).toFixed(control.decimals))
return (tmpMinStepSize) ? tmpMinStepSize : control.realStepSize
}
property real maxStepSize: {
var tmpMaxStepSize = Number((control.realStepSize * 10.0).toFixed(control.decimals))
return (tmpMaxStepSize < control.realTo) ? tmpMaxStepSize : control.realStepSize
}
property bool edit: spinBoxInput.activeFocus
// This property is used to indicate the global hover state
property bool hover: (spinBoxInput.hover || spinBoxIndicatorUp.hover || spinBoxIndicatorDown.hover)
&& control.enabled
property bool dirty: false // user modification flag
property bool spinBoxIndicatorVisible: true
property real __spinBoxIndicatorWidth: control.style.spinBoxIndicatorSize.width
property real __spinBoxIndicatorHeight: control.height / 2 - control.style.borderWidth
property alias compressedValueTimer: myTimer
property string preFocusText: ""
signal realValueModified
signal compressedRealValueModified
signal indicatorPressed
locale: Utils.locale
// Use custom wheel handling due to bugs
property bool __wheelEnabled: false
wheelEnabled: false
hoverEnabled: true
width: control.style.controlSize.width
height: control.style.controlSize.height
leftPadding: spinBoxIndicatorDown.x + spinBoxIndicatorDown.width
rightPadding: control.style.borderWidth
font.pixelSize: control.style.baseFontSize
editable: true
// Leave this in for now
from: -99
value: 0
to: 99
function checkAndClearFocus() {
if (!spinBoxIndicatorUp.activeFocus && !spinBoxIndicatorDown.activeFocus && !spinBoxInput.activeFocus)
control.focus = false
}
DoubleValidator {
id: doubleValidator
locale: control.locale
notation: DoubleValidator.StandardNotation
decimals: control.decimals
bottom: Math.min(control.realFrom, control.realTo)
top: Math.max(control.realFrom, control.realTo)
}
IntValidator {
id: intValidator
locale: control.locale
bottom: Math.round(Math.min(control.realFrom, control.realTo))
top: Math.round(Math.max(control.realFrom, control.realTo))
}
validator: control.decimals === 0 ? intValidator : doubleValidator
up.indicator: SpinBoxIndicator {
id: spinBoxIndicatorUp
style: control.style
parentHover: control.hover
parentEdit: control.edit
iconFlip: -1
visible: control.spinBoxIndicatorVisible
onRealPressed: control.indicatorPressed()
onRealReleased: control.realIncrease()
onRealPressAndHold: control.realIncrease()
x: control.style.borderWidth
y: control.style.borderWidth
width: control.spinBoxIndicatorVisible ? control.__spinBoxIndicatorWidth : 0
height: control.spinBoxIndicatorVisible ? control.__spinBoxIndicatorHeight : 0
realEnabled: (control.realFrom < control.realTo) ? (control.realValue < control.realTo)
: (control.realValue > control.realTo)
}
down.indicator: SpinBoxIndicator {
id: spinBoxIndicatorDown
style: control.style
parentHover: control.hover
parentEdit: control.edit
visible: control.spinBoxIndicatorVisible
onRealPressed: control.indicatorPressed()
onRealReleased: control.realDecrease()
onRealPressAndHold: control.realDecrease()
x: control.style.borderWidth
y: spinBoxIndicatorUp.y + spinBoxIndicatorUp.height
width: control.spinBoxIndicatorVisible ? control.__spinBoxIndicatorWidth : 0
height: control.spinBoxIndicatorVisible ? control.__spinBoxIndicatorHeight : 0
realEnabled: (control.realFrom < control.realTo) ? (control.realValue > control.realFrom)
: (control.realValue < control.realFrom)
}
contentItem: SpinBoxInput {
id: spinBoxInput
style: control.style
validator: control.validator
font: control.font
readOnly: !control.editable
inputMethodHints: control.inputMethodHints
function handleEditingFinished() {
control.checkAndClearFocus()
// Keep the dirty state before calling setValueFromInput(),
// it will be set to false (cleared) internally
var valueModified = control.dirty
control.setValueFromInput()
myTimer.stop()
// Only trigger the signal, if the value was modified
if (valueModified)
control.compressedRealValueModified()
}
onEditingFinished: {
spinBoxInput.focus = false
spinBoxInput.handleEditingFinished()
}
onTextEdited: control.dirty = true
}
background: Rectangle {
id: spinBoxBackground
color: control.style.background.idle
border.color: control.style.border.idle
border.width: control.style.borderWidth
x: 0
width: control.width
height: control.height
}
textFromValue: function (value, locale) {
// -128 is the value of QLocale::FloatingPointShortest
return Number(control.realValue).toLocaleString(locale, 'f', -128)
}
valueFromText: function (text, locale) {
control.setRealValue(Number.fromLocaleString(locale, spinBoxInput.text))
return 0
}
states: [
State {
name: "default"
when: control.enabled && !control.hover && !control.hovered
&& !control.edit
PropertyChanges {
target: control
__wheelEnabled: false
}
PropertyChanges {
target: spinBoxInput
selectByMouse: false
}
PropertyChanges {
target: spinBoxBackground
color: control.style.background.idle
border.color: control.style.border.idle
}
},
State {
name: "hover"
when: control.enabled && control.hover && control.hovered
&& !control.edit
PropertyChanges {
target: spinBoxBackground
border.color: control.style.border.hover
}
},
State {
name: "edit"
when: control.edit
PropertyChanges {
target: control
__wheelEnabled: true
}
PropertyChanges {
target: spinBoxInput
selectByMouse: true
}
PropertyChanges {
target: spinBoxBackground
border.color: control.style.border.interaction
}
},
State {
name: "disable"
when: !control.enabled
PropertyChanges {
target: spinBoxBackground
border.color: control.style.border.disabled
}
}
]
Timer {
id: myTimer
repeat: false
running: false
interval: 400
onTriggered: control.compressedRealValueModified()
}
onRealValueChanged: {
control.setRealValue(control.realValue) // sanitize and clamp realValue
spinBoxInput.text = control.textFromValue(control.realValue, control.locale)
control.value = 0 // Without setting value back to 0, it can happen that one of
// the indicator will be disabled due to range logic.
}
onRealValueModified: myTimer.restart()
onFocusChanged: {
if (control.focus)
control.dirty = false
}
onDisplayTextChanged: spinBoxInput.text = control.displayText
onActiveFocusChanged: {
if (control.activeFocus) { // QTBUG-75862 && mySpinBox.focusReason === Qt.TabFocusReason)
control.preFocusText = spinBoxInput.text
spinBoxInput.selectAll()
} else {
// Make sure displayed value is correct after focus loss, as onEditingFinished
// doesn't trigger when value is something validator doesn't accept.
if (spinBoxInput.text === "")
spinBoxInput.text = control.textFromValue(control.realValue, control.locale)
if (control.dirty)
spinBoxInput.handleEditingFinished()
}
}
onDecimalsChanged: spinBoxInput.text = control.textFromValue(control.realValue, control.locale)
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Up || event.key === Qt.Key_Down) {
event.accepted = true
// Store current step size
var currStepSize = control.realStepSize
// Set realStepSize according to used modifier key
if (event.modifiers & Qt.ControlModifier)
control.realStepSize = control.minStepSize
if (event.modifiers & Qt.ShiftModifier)
control.realStepSize = control.maxStepSize
if (event.key === Qt.Key_Up)
control.realIncrease()
else
control.realDecrease()
// Reset realStepSize
control.realStepSize = currStepSize
}
if (event.key === Qt.Key_Escape) {
spinBoxInput.text = control.preFocusText
control.dirty = true
spinBoxInput.handleEditingFinished()
}
}
function clamp(v, lo, hi) {
return (v < lo || v > hi) ? Math.min(Math.max(lo, v), hi) : v
}
function setValueFromInput() {
if (!control.dirty)
return
// FIX: This is a temporary fix for QTBUG-74239
var currValue = control.realValue
// Call the function but don't use return value. The realValue property
// will be implicitly set inside the function/procedure.
control.valueFromText(spinBoxInput.text, control.locale)
if (control.realValue !== currValue) {
control.realValueModified()
} else {
// Check if input text differs in format from the current value
var tmpInputValue = control.textFromValue(control.realValue, control.locale)
if (tmpInputValue !== spinBoxInput.text)
spinBoxInput.text = tmpInputValue
}
control.dirty = false
}
function setRealValue(value) {
if (Number.isNaN(value))
value = 0
if (control.decimals === 0)
value = Math.round(value)
control.realValue = control.clamp(value,
control.validator.bottom,
control.validator.top)
}
function realDecrease() {
// Store the current value for comparison
var currValue = control.realValue
control.valueFromText(spinBoxInput.text, control.locale)
control.setRealValue(control.realValue - control.realStepSize)
if (control.realValue !== currValue)
control.realValueModified()
}
function realIncrease() {
// Store the current value for comparison
var currValue = control.realValue
control.valueFromText(spinBoxInput.text, control.locale)
control.setRealValue(control.realValue + control.realStepSize)
if (control.realValue !== currValue)
control.realValueModified()
}
}

View File

@@ -0,0 +1,247 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import QtQuick.Templates as T
import StudioTheme as StudioTheme
Rectangle {
id: control
property StudioTheme.ControlStyle style: StudioTheme.Values.controlStyle
property bool hover: spinBoxIndicatorMouseArea.containsMouse
property bool pressed: spinBoxIndicatorMouseArea.containsPress
property bool released: false
property bool realEnabled: true
property bool parentHover: false
property bool parentEdit: false
signal realPressed
signal realPressAndHold
signal realReleased
property alias iconFlip: spinBoxIndicatorIconScale.yScale
color: control.style.background.idle
border.width: 0
onEnabledChanged: control.syncEnabled()
onRealEnabledChanged: {
control.syncEnabled()
if (control.realEnabled === false) {
pressAndHoldTimer.stop()
spinBoxIndicatorMouseArea.pressedAndHeld = false
}
}
// This function is meant to synchronize enabled with realEnabled to avoid
// the internal logic messing with the actual state.
function syncEnabled() {
control.enabled = control.realEnabled
}
Timer {
id: pressAndHoldTimer
repeat: true
running: false
interval: 100
onTriggered: control.realPressAndHold()
}
// This MouseArea is a workaround to avoid some hover state related bugs
// when using the actual signal 'up.hovered'. QTBUG-74688
MouseArea {
id: spinBoxIndicatorMouseArea
property bool pressedAndHeld: false
anchors.fill: parent
hoverEnabled: true
pressAndHoldInterval: 500
onPressed: function(mouse) {
//if (control.__parentControl.activeFocus)
// control.forceActiveFocus()
control.realPressed()
mouse.accepted = true
}
onPressAndHold: {
pressAndHoldTimer.restart()
spinBoxIndicatorMouseArea.pressedAndHeld = true
}
onReleased: function(mouse) {
// Only trigger real released when pressAndHold isn't active
if (!pressAndHoldTimer.running && containsMouse)
control.realReleased()
pressAndHoldTimer.stop()
mouse.accepted = true
spinBoxIndicatorMouseArea.pressedAndHeld = false
}
onEntered: {
if (spinBoxIndicatorMouseArea.pressedAndHeld)
pressAndHoldTimer.restart()
}
onExited: {
if (pressAndHoldTimer.running)
pressAndHoldTimer.stop()
}
}
T.Label {
id: spinBoxIndicatorIcon
text: StudioTheme.Constants.upDownSquare2
color: control.style.icon.idle
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: control.style.smallIconFontSize
font.family: StudioTheme.Constants.iconFont.family
anchors.fill: parent
transform: Scale {
id: spinBoxIndicatorIconScale
origin.x: 0
origin.y: spinBoxIndicatorIcon.height / 2
yScale: 1
}
states: [
State {
name: "default"
when: control.enabled && !control.hover && !control.parentEdit && !control.parentHover
PropertyChanges {
target: spinBoxIndicatorIcon
color: control.style.icon.idle
}
},
State {
name: "globalHover"
when: control.enabled && !control.hover && !control.parentEdit && control.parentHover
PropertyChanges {
target: spinBoxIndicatorIcon
color: control.style.icon.idle
}
},
State {
name: "hover"
when: control.enabled && control.hover && !control.pressed && control.parentHover
PropertyChanges {
target: spinBoxIndicatorIcon
color: control.style.icon.hover
}
},
State {
name: "press"
when: control.enabled && control.pressed
PropertyChanges {
target: spinBoxIndicatorIcon
color: control.style.icon.idle
}
},
State {
name: "parentEdit"
when: control.parentEdit && control.enabled
PropertyChanges {
target: spinBoxIndicatorIcon
color: control.style.icon.idle
}
},
State {
name: "disable"
when: !control.enabled
PropertyChanges {
target: spinBoxIndicatorIcon
color: control.style.icon.disabled
}
}
]
}
states: [
State {
name: "default"
when: !control.parentEdit && !control.hover && !control.parentHover
PropertyChanges {
target: spinBoxIndicatorIcon
visible: false
}
PropertyChanges {
target: control
color: control.style.background.idle
}
},
State {
name: "globalHover"
when: control.enabled && !control.hover && !control.parentEdit && control.parentHover
PropertyChanges {
target: spinBoxIndicatorIcon
visible: true
}
PropertyChanges {
target: control
color: control.style.background.globalHover
}
},
State {
name: "hover"
when: control.enabled && control.hover && !control.pressed && control.parentHover
PropertyChanges {
target: spinBoxIndicatorIcon
visible: true
}
PropertyChanges {
target: control
color: control.style.background.hover
}
},
State {
name: "press"
when: control.enabled && control.pressed
PropertyChanges {
target: spinBoxIndicatorIcon
visible: true
}
PropertyChanges {
target: control
color: control.style.interaction
}
},
State {
name: "parentEdit"
when: control.parentEdit && control.enabled
PropertyChanges {
target: spinBoxIndicatorIcon
visible: true
}
PropertyChanges {
target: control
color: control.style.background.idle
}
},
State {
name: "disable"
when: !control.enabled
PropertyChanges {
target: spinBoxIndicatorIcon
visible: false
}
PropertyChanges {
target: control
color: control.style.background.disabled
}
},
State {
name: "limit"
when: !control.enabled && !control.realEnabled && control.parentHover
PropertyChanges {
target: spinBoxIndicatorIcon
visible: true
}
PropertyChanges {
target: control
color: control.style.background.idle
}
}
]
}

View File

@@ -0,0 +1,100 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import QtQuick.Templates as T
import StudioTheme as StudioTheme
TextInput {
id: control
property StudioTheme.ControlStyle style: StudioTheme.Values.controlStyle
property bool edit: control.activeFocus
property bool drag: false
property bool hover: hoverHandler.hovered && control.enabled
z: 2
color: control.style.text.idle
selectionColor: control.style.text.selection
selectedTextColor: control.style.text.selectedText
horizontalAlignment: Qt.AlignLeft
verticalAlignment: Qt.AlignVCenter
leftPadding: control.style.inputHorizontalPadding
rightPadding: control.style.inputHorizontalPadding
selectByMouse: false
activeFocusOnPress: false
clip: true
// TextInput focus needs to be set to activeFocus whenever it changes,
// otherwise TextInput will get activeFocus whenever the parent SpinBox gets
// activeFocus. This will lead to weird side effects.
onActiveFocusChanged: control.focus = control.activeFocus
HoverHandler { id: hoverHandler }
Rectangle {
id: textInputBackground
x: 0
y: control.style.borderWidth
z: -1
width: control.width
height: control.height - (control.style.borderWidth * 2)
color: control.style.background.idle
border.width: 0
}
// Ensure that we get Up and Down key press events first
Keys.onShortcutOverride: function(event) {
event.accepted = (event.key === Qt.Key_Up || event.key === Qt.Key_Down)
}
states: [
State {
name: "default"
when: control.enabled && !control.edit && !control.hover
PropertyChanges {
target: textInputBackground
color: control.style.background.idle
}
},
State {
name: "globalHover"
when: !control.hover && !control.edit
PropertyChanges {
target: textInputBackground
color: control.style.background.globalHover
}
},
State {
name: "hover"
when: control.hover && !control.edit
PropertyChanges {
target: textInputBackground
color: control.style.background.hover
}
},
State {
name: "edit"
when: control.edit
PropertyChanges {
target: textInputBackground
color: control.style.background.interaction
}
},
State {
name: "disable"
when: !control.enabled
PropertyChanges {
target: textInputBackground
color: control.style.background.disabled
}
PropertyChanges {
target: control
color: control.style.text.disabled
}
}
]
}

View File

@@ -4,7 +4,7 @@
import QtQuick
import QtQuick.Templates as T
import StudioTheme 1.0 as StudioTheme
import StudioTheme as StudioTheme
T.Switch {
id: control
@@ -14,6 +14,7 @@ T.Switch {
// This property is used to indicate the global hover state
property bool hover: control.hovered && control.enabled
property bool edit: false
property bool readonly: false
property alias labelVisible: label.visible
property alias labelColor: label.color
@@ -33,6 +34,8 @@ T.Switch {
hoverEnabled: true
activeFocusOnTab: false
enabled: !control.readonly
indicator: Rectangle {
id: switchBackground
x: 0
@@ -60,6 +63,7 @@ T.Switch {
color: control.style.icon.idle
border.width: 0
}
}
contentItem: T.Label {
@@ -133,7 +137,7 @@ T.Switch {
},
State {
name: "disable"
when: !control.enabled && !control.checked
when: !control.enabled && !control.checked && !control.readonly
PropertyChanges {
target: switchBackground
color: control.style.background.disabled
@@ -186,8 +190,19 @@ T.Switch {
},
State {
name: "disableChecked"
when: !control.enabled && control.checked
when: !control.enabled && control.checked && !control.readonly
extend: "disable"
},
State {
name: "readonly"
when: !control.enabled && !control.checked && control.readonly
extend: "default"
},
State {
name: "readonlyChecked"
when: !control.enabled && control.checked && control.readonly
extend: "defaultChecked"
}
]
}

View File

@@ -1,3 +1,7 @@
BindingIndicator 1.0 BindingIndicator.qml
MenuItem 1.0 MenuItem.qml
SpinBox 1.0 SpinBox.qml
SpinBoxIndicator 1.0 SpinBoxIndicator.qml
SpinBoxInput 1.0 SpinBoxInput.qml
Switch 1.0 Switch.qml
TextField 1.0 TextField.qml

View File

@@ -596,14 +596,18 @@ Item {
anchors.centerIn: parent
text: {
if (!materialBrowserModel.isQt6Project)
if (!materialBrowserModel.isQt6Project) {
qsTr("<b>Material Browser</b> is not supported in Qt5 projects.")
else if (!materialBrowserModel.hasQuick3DImport)
qsTr("To use <b>Material Browser</b>, first add the QtQuick3D module in the <b>Components</b> view.")
else if (!materialBrowserModel.hasMaterialLibrary)
} else if (!materialBrowserModel.hasQuick3DImport) {
qsTr('To use the <b>Material Browser</b>, add the <b>QtQuick3D</b> module and the <b>View3D</b>
component in the <b>Components</b> view, or click
<a href=\"#add_import\"><span style=\"text-decoration:none;color:%1\">
here</span></a>.').arg(StudioTheme.Values.themeInteraction)
} else if (!materialBrowserModel.hasMaterialLibrary) {
qsTr("<b>Material Browser</b> is disabled inside a non-visual component.")
else
} else {
""
}
}
textFormat: Text.RichText
@@ -611,6 +615,8 @@ Item {
font.pixelSize: StudioTheme.Values.mediumFontSize
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
onLinkActivated: rootView.addQtQuick3D()
}
}

View File

@@ -186,6 +186,8 @@ Section {
spacing: 1
Section {
readonly property bool __isInEffectsSection: true // used by property search logic
sectionHeight: 37
anchors.left: parent.left
anchors.right: parent.right
@@ -232,6 +234,8 @@ Section {
}
Section {
readonly property bool __isInEffectsSection: true // used by property search logic
sectionHeight: 37
anchors.left: parent.left
anchors.right: parent.right
@@ -305,6 +309,8 @@ Section {
Section {
id: delegate
readonly property bool __isInEffectsSection: true // used by property search logic
property QtObject wrapper: modelNodeBackend.registerSubSelectionWrapper(modelData)
property bool wasExpanded: false

View File

@@ -20,7 +20,7 @@ PropertyEditorPane {
}
DynamicPropertiesSection {
propertiesModel: SelectionDynamicPropertiesModel {}
propertiesModel: PropertyEditorDynamicPropertiesModel {}
visible: !hasMultiSelection
}
@@ -90,9 +90,17 @@ PropertyEditorPane {
StudioControls.TabButton {
text: backendValues.__classNamePrivateInternal.value
onClicked: () => {
if (itemPane.searchBar.hasDoneSearch)
itemPane.searchBar.search();
}
}
StudioControls.TabButton {
text: qsTr("Layout")
onClicked: () => {
if (itemPane.searchBar.hasDoneSearch)
itemPane.searchBar.search();
}
}
}

View File

@@ -12,7 +12,7 @@ PropertyEditorPane {
ComponentSection {}
DynamicPropertiesSection {
propertiesModel: SelectionDynamicPropertiesModel {}
propertiesModel: PropertyEditorDynamicPropertiesModel {}
visible: !hasMultiSelection
}

View File

@@ -12,4 +12,9 @@ Column {
AnimationSection {
showDuration: false
}
ScriptSection {
anchors.left: parent.left
anchors.right: parent.right
}
}

View File

@@ -0,0 +1,102 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import QtQuick.Controls
import HelperWidgets
import StudioControls as StudioControls
import StudioTheme as StudioTheme
import ScriptEditorBackend
import ScriptsEditor as ScriptsEditor
Section {
id: root
caption: qsTr("Script")
SectionLayout {
PropertyLabel {
text: ""
tooltip: ""
}
SecondColumnLayout {
Spacer { implicitWidth: StudioTheme.Values.actionIndicatorWidth }
AbstractButton {
id: editScriptButton
implicitWidth: StudioTheme.Values.singleControlColumnWidth
width: StudioTheme.Values.singleControlColumnWidth
buttonIcon: qsTr("Edit script")
iconFontFamily: StudioTheme.Constants.font.family
onClicked: dialog.show(root)
}
ExpandingSpacer {}
}
}
data: [
StudioControls.PopupDialog {
id: dialog
readonly property real horizontalSpacing: 10
readonly property real verticalSpacing: 12
readonly property real columnWidth: (dialog.width - dialog.horizontalSpacing - (2 * dialog.anchorGap)) / 2
property var backend: ScriptEditorBackend
titleBar: Row {
id: titleBarRow
spacing: 30
anchors.fill: parent
Text {
color: StudioTheme.Values.themeTextColor
text: qsTr("Action")
font.pixelSize: StudioTheme.Values.myFontSize
anchors.verticalCenter: parent.verticalCenter
ToolTipArea {
anchors.fill: parent
tooltip: qsTr("Sets an action that is associated with the selected <b>ScriptAction</b>.")
}
}
ScriptsEditor.ActionsComboBox {
id: action
style: StudioTheme.Values.connectionPopupControlStyle
width: 180
anchors.verticalCenter: parent.verticalCenter
backend: dialog.backend
}
}
ScriptsEditor.ScriptEditorForm {
id: scriptEditor
anchors.left: parent.left
anchors.right: parent.right
horizontalSpacing: dialog.horizontalSpacing
verticalSpacing: dialog.verticalSpacing
columnWidth: dialog.columnWidth
spacing: dialog.verticalSpacing
backend: dialog.backend
currentAction: action.currentValue ?? StatementDelegate.Custom
}
},
Connections {
target: modelNodeBackend
function onSelectionChanged() {
dialog.backend.update()
}
},
TapHandler {
onTapped: scriptEditor.forceActiveFocus()
}
]
}

View File

@@ -13,6 +13,11 @@ Rectangle {
height: 400
color: StudioTheme.Values.themePanelBackground
// Called from C++ to clear the search when the selected node changes
function clearSearch() {
// The function is empty, because it is a placeholder to match other panes
}
ColumnLayout {
id: mainColumn
anchors.fill: parent

View File

@@ -1,15 +0,0 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
import QtQuick 2.15
import QtQuick.Layouts 1.15
import HelperWidgets 2.0
Column {
width: parent.width
// CubeMapTexture inherits Texture but doesn't provide any extra properties itself
TextureSection {
width: parent.width
}
}

View File

@@ -11,8 +11,4 @@ Column {
CustomMaterialSection {
width: parent.width
}
MaterialSection {
width: parent.width
}
}

View File

@@ -11,8 +11,4 @@ Column {
DefaultMaterialSection {
width: parent.width
}
MaterialSection {
width: parent.width
}
}

View File

@@ -0,0 +1,62 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import StudioControls as StudioControls
StudioControls.PopupDialog {
id: colorPopup
property QtObject loaderItem: loader.item
property color originalColor
required property color currentColor
signal activateColor(color : color)
width: 260
onOriginalColorChanged: loader.updateOriginalColor()
onClosing: loader.active = false
function open(showItem) {
loader.ensureActive()
colorPopup.show(showItem)
loader.updateOriginalColor()
}
Loader {
id: loader
function ensureActive() {
if (!loader.active)
loader.active = true
}
function updateOriginalColor() {
if (loader.status === Loader.Ready)
loader.item.originalColor = colorPopup.originalColor
}
sourceComponent: StudioControls.ColorEditorPopup {
width: colorPopup.contentWidth
visible: colorPopup.visible
onActivateColor: (color) => {
colorPopup.activateColor(color)
}
}
Binding {
target: loader.item
property: "color"
value: colorPopup.currentColor
when: loader.status === Loader.Ready
}
onLoaded: {
loader.updateOriginalColor()
colorPopup.titleBar = loader.item.titleBarContent
}
}
}

View File

@@ -0,0 +1,266 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import HelperWidgets 2.0 as HelperWidgets
import StudioControls as StudioControls
import StudioTheme as StudioTheme
Rectangle {
id: root
property HelperWidgets.QmlMaterialNodeProxy backend: materialNodeBackend
property alias pinned: pinButton.checked
property alias showPinButton: pinButton.visible
property StudioTheme.ControlStyle buttonStyle: StudioTheme.ViewBarButtonStyle {
// This is how you can override stuff from the control styles
baseIconFontSize: StudioTheme.Values.bigIconFontSize
}
Connections {
target: HelperWidgets.Controller
function onCloseContextMenu() {
root.closeContextMenu()
}
}
implicitHeight: image.height
clip: true
color: "#000000"
// Called from C++ to close context menu on focus out
function closeContextMenu()
{
modelMenu.close()
envMenu.close()
}
function refreshPreview()
{
image.source = ""
image.source = "image://nodeInstance/preview"
}
Connections {
target: root.backend
function onPreviewEnvChanged() {
envMenu.updateEnvParams(backend.previewEnv)
root.refreshPreview()
}
function onPreviewModelChanged() {
root.refreshPreview()
}
}
Image {
id: image
anchors.fill: parent
fillMode: Image.PreserveAspectFit
source: "image://nodeInstance/preview"
cache: false
smooth: true
sourceSize.width: image.width
sourceSize.height: image.height
Rectangle {
id: toolbarRect
radius: 10
color: StudioTheme.Values.themeToolbarBackground
width: optionsToolbar.width + 2 * toolbarRect.radius
height: optionsToolbar.height + toolbarRect.radius
anchors.left: parent.left
anchors.leftMargin: -toolbarRect.radius
anchors.verticalCenter: parent.verticalCenter
Column {
id: optionsToolbar
spacing: 5
anchors.centerIn: parent
anchors.horizontalCenterOffset: optionsToolbar.spacing
HelperWidgets.AbstractButton {
id: pinButton
style: root.buttonStyle
buttonIcon: pinButton.checked ? StudioTheme.Constants.pin : StudioTheme.Constants.unpin
checkable: true
}
HelperWidgets.AbstractButton {
id: previewEnvMenuButton
style: root.buttonStyle
buttonIcon: StudioTheme.Constants.textures_medium
tooltip: qsTr("Select preview environment.")
onClicked: envMenu.popup()
}
HelperWidgets.AbstractButton {
id: previewModelMenuButton
style: root.buttonStyle
buttonIcon: StudioTheme.Constants.cube_medium
tooltip: qsTr("Select preview model.")
onClicked: modelMenu.popup()
}
}
}
}
StudioControls.Menu {
id: modelMenu
closePolicy: StudioControls.Menu.CloseOnEscape | StudioControls.Menu.CloseOnPressOutside
ListModel {
id: modelMenuModel
ListElement {
modelName: qsTr("Cone")
modelStr: "#Cone"
}
ListElement {
modelName: qsTr("Cube")
modelStr: "#Cube"
}
ListElement {
modelName: qsTr("Cylinder")
modelStr: "#Cylinder"
}
ListElement {
modelName: qsTr("Sphere")
modelStr: "#Sphere"
}
}
Repeater {
model: modelMenuModel
StudioControls.MenuItemWithIcon {
text: modelName
onClicked: root.backend.previewModel = modelStr
checkable: true
checked: root.backend.previewModel === modelStr
}
}
}
StudioControls.Menu {
id: envMenu
property string previewEnvName
property string previewEnvValue
signal envParametersChanged()
closePolicy: StudioControls.Menu.CloseOnEscape | StudioControls.Menu.CloseOnPressOutside
Component.onCompleted: envMenu.updateEnvParams(root.backend.previewEnv)
function updateEnvParams(str: string) {
let eqFound = str.lastIndexOf("=")
let newEnvName = (eqFound > 0) ? str.substr(0, eqFound) : str
let newEnvValue = (eqFound > 0) ? str.substr(eqFound + 1, str.length - eqFound) : ""
if (envMenu.previewEnvName !== newEnvName
|| envMenu.previewEnvValue !== newEnvValue) {
envMenu.previewEnvName = newEnvName
envMenu.previewEnvValue = newEnvValue
envMenu.envParametersChanged()
}
}
EnvMenuItem {
envName: qsTr("Basic")
envStr: "Basic"
}
EnvMenuItem {
id: colorItem
property color color
property bool colorIsValid: false
envName: qsTr("Color")
envStr: "Color"
checked: false
Component.onCompleted: update()
onColorIsValidChanged: updatePopupOriginalColor()
onClicked: {
colorItem.updatePopupOriginalColor()
colorPopup.open(colorItem)
}
onColorChanged: {
colorItem.envStr = colorItem.checked
? "Color=" + color.toString()
: "Color"
colorItem.commit()
}
function updatePopupOriginalColor() {
if (colorItem.colorIsValid)
colorPopup.originalColor = colorItem.color
}
function update() {
colorItem.checked = envMenu.previewEnvName === "Color"
if (colorItem.checked && envMenu.previewEnvValue) {
colorItem.color = envMenu.previewEnvValue
colorItem.colorIsValid = true
} else {
colorItem.colorIsValid = false
}
}
Connections {
target: envMenu
function onEnvParametersChanged() {
colorItem.update();
}
}
}
EnvMenuItem {
envName: qsTr("Studio")
envStr: "SkyBox=preview_studio"
}
EnvMenuItem {
envName: qsTr("Landscape")
envStr: "SkyBox=preview_landscape"
}
}
ColorEditorPopup {
id: colorPopup
currentColor: colorItem.color
onActivateColor: (color) => colorItem.color = color
}
component EnvMenuItem: StudioControls.MenuItemWithIcon {
required property string envName
property string envStr
function commit() {
root.backend.previewEnv = envStr
}
text: envName
onClicked: commit()
checkable: false
checked: root.backend.previewEnv === envStr
}
}

View File

@@ -0,0 +1,50 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import HelperWidgets 2.0
import HelperWidgets 2.0 as HelperWidgets
import StudioTheme as StudioTheme
Rectangle {
id: root
property HelperWidgets.QmlMaterialNodeProxy backend: materialNodeBackend
color: StudioTheme.Values.themeToolbarBackground
height: StudioTheme.Values.toolbarHeight
Row {
id: row
spacing: StudioTheme.Values.toolbarSpacing
anchors.verticalCenter: parent.verticalCenter
leftPadding: 6
HelperWidgets.AbstractButton {
style: StudioTheme.Values.viewBarButtonStyle
buttonIcon: StudioTheme.Constants.apply_medium
tooltip: qsTr("Apply material to selected model.")
onClicked: root.backend.toolBarAction(QmlMaterialNodeProxy.ApplyToSelected)
}
HelperWidgets.AbstractButton {
style: StudioTheme.Values.viewBarButtonStyle
buttonIcon: StudioTheme.Constants.create_medium
tooltip: qsTr("Create new material.")
onClicked: backend.toolBarAction(QmlMaterialNodeProxy.AddNewMaterial)
}
HelperWidgets.AbstractButton {
style: StudioTheme.Values.viewBarButtonStyle
buttonIcon: StudioTheme.Constants.delete_medium
tooltip: qsTr("Delete current material.")
onClicked: backend.toolBarAction(QmlMaterialNodeProxy.DeleteCurrentMaterial)
}
HelperWidgets.AbstractButton {
style: StudioTheme.Values.viewBarButtonStyle
buttonIcon: StudioTheme.Constants.materialBrowser_medium
tooltip: qsTr("Open material browser.")
onClicked: backend.toolBarAction(QmlMaterialNodeProxy.OpenMaterialBrowser)
}
}
}

View File

@@ -0,0 +1,93 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import QtQuick.Controls
import HelperWidgets 2.0 as HelperWidgets
import StudioControls as StudioControls
import StudioTheme as StudioTheme
StudioControls.SplitView {
id: root
property HelperWidgets.QmlMaterialNodeProxy backend: materialNodeBackend
property alias showImage: previewLoader.active
property Component previewComponent: null
width: parent.width
implicitHeight: showImage ? previewLoader.implicitHeight + nameSection.implicitHeight : nameSection.implicitHeight
orientation: Qt.Vertical
Loader {
id: previewLoader
SplitView.fillWidth: true
SplitView.minimumWidth: 152
SplitView.preferredHeight: previewLoader.visible ? Math.min(root.width * 0.75, 400) : 0
SplitView.minimumHeight: previewLoader.visible ? 150 : 0
SplitView.maximumHeight: previewLoader.visible ? 600 : 0
visible: previewLoader.active && previewLoader.item
sourceComponent: root.previewComponent
}
HelperWidgets.Section {
id: nameSection
// Section with hidden header is used so properties are aligned with the other sections' properties
hideHeader: true
SplitView.fillWidth: true
SplitView.preferredHeight: implicitHeight
SplitView.maximumHeight: implicitHeight
bottomPadding: StudioTheme.Values.sectionPadding * 2
collapsible: false
HelperWidgets.SectionLayout {
HelperWidgets.PropertyLabel { text: qsTr("Name") }
HelperWidgets.SecondColumnLayout {
HelperWidgets.Spacer { implicitWidth: StudioTheme.Values.actionIndicatorWidth }
HelperWidgets.LineEdit {
id: matName
implicitWidth: StudioTheme.Values.singleControlColumnWidth
width: StudioTheme.Values.singleControlColumnWidth
placeholderText: qsTr("Material name")
showTranslateCheckBox: false
showExtendedFunctionButton: false
Timer {
running: true
interval: 0
onTriggered: matName.backendValue = backendValues.objectName
// backendValues.objectName is not available yet without the Timer
}
// allow only alphanumeric characters, underscores, no space at start, and 1 space between words
validator: RegularExpressionValidator { regularExpression: /^(\w+\s)*\w+$/ }
}
HelperWidgets.ExpandingSpacer {}
}
HelperWidgets.PropertyLabel { text: qsTr("Type") }
HelperWidgets.SecondColumnLayout {
HelperWidgets.Spacer { implicitWidth: StudioTheme.Values.actionIndicatorWidth }
HelperWidgets.ComboBox {
currentIndex: backend.possibleTypeIndex
model: backend.possibleTypes
showExtendedFunctionButton: false
implicitWidth: StudioTheme.Values.singleControlColumnWidth
enabled: backend.possibleTypes.length > 1
onActivated: changeTypeName(currentValue)
}
}
}
}
}

View File

@@ -0,0 +1,137 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtCore
import QtQuick
import QtQuick.Controls
import HelperWidgets 2.0
import StudioControls 1.0 as StudioControls
import "Material" as Material
Item {
id: root
width: 420
height: 420
// invoked from C++ to refresh material preview image
signal refreshPreview()
// Called from C++ to close context menu on focus out
function closeContextMenu()
{
Controller.closeContextMenu()
}
Material.Toolbar {
id: toolbar
width: parent.width
}
Settings {
id: settings
property var topSection
property bool dockMode
}
StudioControls.SplitView {
id: splitView
readonly property bool isHorizontal: splitView.orientation == Qt.Horizontal
anchors.top: toolbar.bottom
anchors.bottom: parent.bottom
width: parent.width
orientation: splitView.width > 1000 ? Qt.Horizontal : Qt.Vertical
clip: true
Loader {
id: leftSideView
SplitView.fillWidth: leftSideView.visible
SplitView.fillHeight: leftSideView.visible
SplitView.minimumWidth: leftSideView.visible ? 300 : 0
SplitView.minimumHeight: leftSideView.visible ? 300 : 0
active: splitView.isHorizontal
visible: leftSideView.active && leftSideView.item
sourceComponent: PreviewComponent {}
}
PropertyEditorPane {
id: itemPane
clip: true
SplitView.fillWidth: !leftSideView.visible
SplitView.fillHeight: true
SplitView.minimumWidth: leftSideView.visible ? 400 : 0
SplitView.maximumWidth: leftSideView.visible ? 800 : -1
headerDocked: !leftSideView.visible && settings.dockMode
headerComponent: Material.TopSection {
id: topSection
Component.onCompleted: topSection.restoreState(settings.topSection)
Component.onDestruction: settings.topSection = topSection.saveState()
previewComponent: PreviewComponent {}
showImage: !splitView.isHorizontal
}
DynamicPropertiesSection {
propertiesModel: PropertyEditorDynamicPropertiesModel {}
visible: !hasMultiSelection
}
Loader {
id: specificsTwo
property string theSource: specificQmlData
width: itemPane.width
visible: specificsTwo.theSource !== ""
sourceComponent: specificQmlComponent
onTheSourceChanged: {
specificsTwo.active = false
specificsTwo.active = true
}
}
Item { // spacer
width: 1
height: 10
visible: specificsTwo.visible
}
Loader {
id: specificsOne
width: itemPane.width
source: specificsUrl
}
MaterialSection {
width: itemPane.width
}
}
}
component PreviewComponent : Material.Preview {
id: previewItem
pinned: settings.dockMode
showPinButton: !leftSideView.visible
onPinnedChanged: settings.dockMode = previewItem.pinned
Connections {
target: root
function onRefreshPreview() {
previewItem.refreshPreview()
}
}
}
}

View File

@@ -18,7 +18,7 @@ PropertyEditorPane {
anchors.right: parent.right
DynamicPropertiesSection {
propertiesModel: SelectionDynamicPropertiesModel {}
propertiesModel: PropertyEditorDynamicPropertiesModel {}
visible: !hasMultiSelection
}

View File

@@ -11,8 +11,4 @@ Column {
PrincipledMaterialSection {
width: parent.width
}
MaterialSection {
width: parent.width
}
}

View File

@@ -11,8 +11,4 @@ Column {
SpecularGlossyMaterialSection {
width: parent.width
}
MaterialSection {
width: parent.width
}
}

View File

@@ -0,0 +1,54 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import HelperWidgets 2.0
import StudioTheme as StudioTheme
Rectangle {
id: root
property QmlTextureNodeProxy backend: textureNodeBackend
color: StudioTheme.Values.themeToolbarBackground
implicitHeight: StudioTheme.Values.toolbarHeight
Row {
id: row
spacing: StudioTheme.Values.toolbarSpacing
anchors.verticalCenter: parent.verticalCenter
leftPadding: 6
AbstractButton {
style: StudioTheme.Values.viewBarButtonStyle
buttonIcon: StudioTheme.Constants.apply_medium
enabled: backend.hasTexture && backend.selectedNodeAcceptsMaterial && hasQuick3DImport && hasMaterialLibrary
tooltip: qsTr("Apply texture to selected model's material.")
onClicked: backend.toolbarAction(QmlTextureNodeProxy.ApplyToSelected)
}
AbstractButton {
style: StudioTheme.Values.viewBarButtonStyle
buttonIcon: StudioTheme.Constants.create_medium
enabled: hasQuick3DImport && hasMaterialLibrary
tooltip: qsTr("Create new texture.")
onClicked: backend.toolbarAction(QmlTextureNodeProxy.AddNewTexture)
}
AbstractButton {
style: StudioTheme.Values.viewBarButtonStyle
buttonIcon: StudioTheme.Constants.delete_medium
enabled: backend.hasTexture && hasQuick3DImport && hasMaterialLibrary
tooltip: qsTr("Delete current texture.")
onClicked: backend.toolbarAction(QmlTextureNodeProxy.DeleteCurrentTexture)
}
AbstractButton {
style: StudioTheme.Values.viewBarButtonStyle
buttonIcon: StudioTheme.Constants.materialBrowser_medium
enabled: backend.hasTexture && hasQuick3DImport && hasMaterialLibrary
tooltip: qsTr("Open material browser.")
onClicked: backend.toolbarAction(QmlTextureNodeProxy.OpenMaterialBrowser)
}
}
}

View File

@@ -0,0 +1,88 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import HelperWidgets 2.0 as HelperWidgets
import StudioTheme as StudioTheme
Rectangle {
id: root
property HelperWidgets.QmlTextureNodeProxy backend: textureNodeBackend
readonly property string sourcePath: backendValues.source ? backendValues.source.valueToString : ""
readonly property string previewPath: "image://qmldesigner_thumbnails/" + backend.resolveResourcePath(root.sourcePath)
function refreshPreview()
{
texturePreview.source = ""
texturePreview.source = root.previewPath
}
color: StudioTheme.Values.themePanelBackground
implicitHeight: column.height
Column {
id: column
Item { implicitWidth: 1; implicitHeight: 10 } // spacer
Rectangle {
id: previewRect
anchors.horizontalCenter: parent.horizontalCenter
width: 152
height: 152
color: "#000000"
Image {
id: texturePreview
asynchronous: true
width: 150
height: 150
fillMode: Image.PreserveAspectFit
anchors.centerIn: parent
source: root.previewPath
}
}
HelperWidgets.Section {
id: nameSection
// Section with hidden header is used so properties are aligned with the other sections' properties
hideHeader: true
implicitWidth: root.width
bottomPadding: StudioTheme.Values.sectionPadding * 2
collapsible: false
HelperWidgets.SectionLayout {
HelperWidgets.PropertyLabel { text: qsTr("Name") }
HelperWidgets.SecondColumnLayout {
HelperWidgets.Spacer { implicitWidth: StudioTheme.Values.actionIndicatorWidth }
HelperWidgets.LineEdit {
id: texName
implicitWidth: StudioTheme.Values.singleControlColumnWidth
width: StudioTheme.Values.singleControlColumnWidth
placeholderText: qsTr("Texture name")
showTranslateCheckBox: false
showExtendedFunctionButton: false
Timer {
running: true
interval: 0
onTriggered: texName.backendValue = backendValues.objectName
// backendValues.objectName is not available yet without the Timer
}
// allow only alphanumeric characters, underscores, no space at start, and 1 space between words
validator: RegularExpressionValidator { regularExpression: /^(\w+\s)*\w+$/ }
}
HelperWidgets.ExpandingSpacer {}
}
}
}
}
}

View File

@@ -0,0 +1,86 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import QtQuick.Layouts
import HelperWidgets 2.0
import StudioTheme as StudioTheme
import "Texture" as Texture
Rectangle {
id: itemPane
width: 420
height: 420
color: StudioTheme.Values.themePanelBackground
// invoked from C++ to refresh material preview image
function refreshPreview()
{
topSection.refreshPreview()
}
// Called from C++ to close context menu on focus out
function closeContextMenu()
{
Controller.closeContextMenu()
}
ColumnLayout {
anchors.fill: parent
spacing: 0
Texture.ToolBar {
Layout.fillWidth: true
}
Texture.TopSection {
id: topSection
Layout.fillWidth: true
}
PropertyEditorPane {
Layout.fillWidth: true
Layout.fillHeight: true
DynamicPropertiesSection {
propertiesModel: PropertyEditorDynamicPropertiesModel {}
visible: !hasMultiSelection
}
Loader {
id: specificsTwo
property string theSource: specificQmlData
anchors.left: parent.left
anchors.right: parent.right
visible: specificsTwo.theSource !== ""
sourceComponent: specificQmlComponent
onTheSourceChanged: {
specificsTwo.active = false
specificsTwo.active = true
}
}
Item {
width: 1
height: 10
visible: specificsTwo.visible
}
Loader {
id: specificsOne
anchors.left: parent.left
anchors.right: parent.right
source: specificsUrl
}
TextureSection {
width: itemPane.width
}
}
}
}

View File

@@ -1,14 +0,0 @@
// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
import QtQuick 2.15
import QtQuick.Layouts 1.15
import HelperWidgets 2.0
Column {
width: parent.width
TextureSection {
width: parent.width
}
}

View File

@@ -507,6 +507,8 @@ Section {
}
PropertyLabel {
readonly property bool __inDynamicPropertiesSection: true
text: propertyName
tooltip: propertyType
Layout.alignment: Qt.AlignTop

View File

@@ -24,9 +24,7 @@ Item {
property real __actionIndicatorWidth: StudioTheme.Values.squareComponentWidth
property real __actionIndicatorHeight: StudioTheme.Values.height
property string typeFilter: "QtQuick3D.Material"
// This binding is a workaround to overcome the rather long adaption to new Qt versions. This
// should actually be fixed in the ModelSection.qml by setting the textRole: "idAndName".
property string textRole: (root.typeFilter === "QtQuick3D.Material") ? "idAndName" : "id"
property string textRole: "id"
property string valueRole: "id"
property int activatedReason: ComboBox.ActivatedReason.Other

View File

@@ -18,6 +18,7 @@ Rectangle {
default property alias content: mainColumn.children
property alias scrollView: mainScrollView
property alias searchBar: propertySearchBar
property bool headerDocked: false
readonly property Item headerItem: headerDocked ? dockedHeaderLoader.item : undockedHeaderLoader.item
@@ -29,10 +30,23 @@ Rectangle {
Controller.closeContextMenu()
}
// Called from C++ to clear the search when the selected node changes
function clearSearch() {
propertySearchBar.clear();
}
PropertySearchBar {
id: propertySearchBar
contentItem: mainColumn
width: parent.width
z: parent.z + 1
}
Loader {
id: dockedHeaderLoader
anchors.top: itemPane.top
anchors.top: propertySearchBar.bottom
z: parent.z + 1
height: item ? item.implicitHeight : 0
width: parent.width
@@ -123,6 +137,16 @@ Rectangle {
HeaderBackground{}
}
Label {
Layout.fillWidth: true
Layout.leftMargin: 10
visible: propertySearchBar.hasDoneSearch && !propertySearchBar.hasMatchSearch
text: qsTr("No match found.")
color: StudioTheme.Values.themeTextColor
font.pixelSize: StudioTheme.Values.baseFont
}
Column {
id: mainColumn

View File

@@ -9,10 +9,13 @@ import StudioTheme 1.0 as StudioTheme
T.Label {
id: label
readonly property bool __isPropertyLabel: true // used by property search logic
property alias tooltip: toolTipArea.tooltip
property bool blockedByContext: false
property bool blockedByTemplate: false // MCU
property bool searchNoMatch: false
width: StudioTheme.Values.propertyLabelWidth
color: StudioTheme.Values.themeTextColor
@@ -34,6 +37,14 @@ T.Label {
}
states: [
State {
name: "searchNoMatch"
when: searchNoMatch
PropertyChanges {
target: label
visible: false
}
},
State {
name: "disabled"
when: !label.enabled && !(label.blockedByContext || label.blockedByTemplate)

View File

@@ -0,0 +1,135 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import StudioControls as StudioControls
import StudioTheme as StudioTheme
Rectangle {
id: root
readonly property alias hasMatchSearch: internal.matched
readonly property alias hasDoneSearch: internal.searched
property Item contentItem: null
color: StudioTheme.Values.themeToolbarBackground
height: StudioTheme.Values.toolbarHeight
function clear() {
internal.clear();
searchBox.text = "";
internal.timer.stop();
}
function search() {
internal.search();
}
StudioControls.SearchBox {
id: searchBox
anchors.fill: parent
anchors.bottomMargin: StudioTheme.Values.toolbarVerticalMargin
anchors.topMargin: StudioTheme.Values.toolbarVerticalMargin
anchors.leftMargin: StudioTheme.Values.toolbarHorizontalMargin
anchors.rightMargin: StudioTheme.Values.toolbarHorizontalMargin
style: StudioTheme.Values.searchControlStyle
onSearchChanged: internal.timer.restart()
}
QtObject {
id: internal
readonly property var reverts: []
readonly property Timer timer: Timer {
interval: 300
repeat: false
onTriggered: internal.search()
}
property bool matched: false
property bool searched: false
function clear() {
internal.reverts.forEach(revert => revert());
internal.reverts.length = 0;
internal.matched = false;
internal.searched = false;
}
function search() {
internal.clear();
const searchText = searchBox.text.toLowerCase();
if (searchText.length > 0) {
internal.traverse(root.contentItem, searchText);
internal.searched = true;
}
}
function disableSearchNoMatchAction(item) {
item.searchNoMatch = true;
internal.reverts.push(() => {
item.searchNoMatch = false;
});
}
function disableVisibleAction(item) {
item.visible = false;
internal.reverts.push(() => {
item.visible = true;
});
}
function enableSearchHideAction(item) {
item.searchHide = true;
internal.reverts.push(() => {
item.searchHide = false;
});
}
function expandSectionAction(item) {
internal.matched = true;
const prevValue = item.expanded;
item.expanded = true;
internal.reverts.push(() => {
item.expanded = prevValue;
});
}
function traverse(item, searchText) {
let hideSection = true;
let hideParentSection = true;
item.children.forEach((child, index, arr) => {
if (!child.visible)
return;
if (child.__isPropertyLabel) {
const propertyLabel = child;
const text = propertyLabel.text.toLowerCase();
if (!text.includes(searchText)) {
internal.disableSearchNoMatchAction(propertyLabel);
const action = propertyLabel.__inDynamicPropertiesSection ? internal.disableVisibleAction
: internal.disableSearchNoMatchAction;
const nextItem = arr[index + 1];
action(nextItem);
} else {
hideSection = false;
}
}
hideSection &= internal.traverse(child, searchText);
if (child.__isSection) {
const action = hideSection ? internal.enableSearchHideAction
: internal.expandSectionAction;
action(child);
if (child.__isInEffectsSection && !hideSection)
hideParentSection = false;
hideSection = true;
}
});
return hideParentSection && hideSection;
}
}
}

View File

@@ -5,6 +5,21 @@ import QtQuick 2.15
import QtQuick.Layouts 1.15
RowLayout {
id: root
property bool searchNoMatch: false
Layout.fillWidth: true
spacing: 0
states: [
State {
name: "searchNoMatch"
when: searchNoMatch
PropertyChanges {
target: root
visible: false
}
}
]
}

View File

@@ -10,6 +10,8 @@ import StudioTheme as StudioTheme
Item {
id: section
readonly property bool __isSection: true // used by property search logic
property string caption: "Title"
property color labelColor: StudioTheme.Values.themeTextColor
property int labelCapitalization: Font.AllUppercase
@@ -58,6 +60,7 @@ Item {
property bool dropEnabled: false
property bool highlight: false
property bool eyeEnabled: true // eye button enabled (on)
property bool searchHide: false
property bool useDefaulContextMenu: true
@@ -343,6 +346,14 @@ Item {
}
states: [
State {
name: "Hide"
when: section.searchHide
PropertyChanges {
target: section
visible: false
}
},
State {
name: "Collapsed"
when: !section.expanded

View File

@@ -1,3 +1,4 @@
module HelperWidgets
AbstractButton 2.0 AbstractButton.qml
ActionIndicator 2.0 ActionIndicator.qml
AlignmentHorizontalButtons 2.0 AlignmentHorizontalButtons.qml
@@ -51,6 +52,7 @@ MultiIconLabel 2.0 MultiIconLabel.qml
OriginControl 2.0 OriginControl.qml
OriginIndicator 2.0 OriginIndicator.qml
OriginSelector 2.0 OriginSelector.qml
PopupLabel 2.0 PopupLabel.qml
PropertyEditorPane 2.0 PropertyEditorPane.qml
PropertyLabel 2.0 PropertyLabel.qml
PaddingSection 2.0 PaddingSection.qml

View File

@@ -3,9 +3,9 @@
import QtQuick
import QtQuick.Templates as T
import StudioTheme 1.0 as StudioTheme
import StudioTheme as StudioTheme
Rectangle {
Item {
id: control
property StudioTheme.ControlStyle style: StudioTheme.Values.controlStyle
@@ -18,8 +18,6 @@ Rectangle {
property bool pressed: false
property bool forceVisible: false
color: "transparent"
implicitWidth: control.style.actionIndicatorSize.width
implicitHeight: control.style.actionIndicatorSize.height

View File

@@ -3,27 +3,29 @@
import QtQuick
import QtQuick.Templates as T
import StudioTheme 1.0 as StudioTheme
import StudioTheme as StudioTheme
T.Button {
id: control
property StudioTheme.ControlStyle style: StudioTheme.Values.controlStyle
implicitWidth: Math.max(buttonBackground ? buttonBackground.implicitWidth : 0,
textItem.implicitWidth + leftPadding + rightPadding)
implicitHeight: Math.max(buttonBackground ? buttonBackground.implicitHeight : 0,
textItem.implicitHeight + topPadding + bottomPadding)
implicitWidth: Math.max(control.style.squareControlSize.width,
implicitBackgroundWidth + leftInset + rightInset,
implicitContentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(control.style.squareControlSize.height,
implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding)
leftPadding: control.style.dialogPadding
rightPadding: control.style.dialogPadding
background: Rectangle {
id: buttonBackground
implicitWidth: 70
implicitHeight: 20
id: controlBackground
color: control.style.background.idle
border.color: control.style.border.idle
anchors.fill: parent
border.width: control.style.borderWidth
radius: control.style.radius
}
contentItem: Text {
@@ -41,7 +43,7 @@ T.Button {
when: control.enabled && !control.down && !control.hovered && !control.checked
PropertyChanges {
target: buttonBackground
target: controlBackground
color: control.highlighted ? control.style.interaction
: control.style.background.idle
border.color: control.style.border.idle
@@ -56,7 +58,7 @@ T.Button {
when: control.enabled && control.hovered && !control.checked && !control.down
PropertyChanges {
target: buttonBackground
target: controlBackground
color: control.style.background.hover
border.color: control.style.border.hover
}
@@ -70,9 +72,9 @@ T.Button {
when: control.enabled && (control.checked || control.down)
PropertyChanges {
target: buttonBackground
color: control.style.background.interaction
border.color: control.style.border.interaction
target: controlBackground
color: control.style.interaction
border.color: control.style.interaction
}
PropertyChanges {
target: textItem
@@ -83,7 +85,7 @@ T.Button {
name: "disable"
when: !control.enabled
PropertyChanges {
target: buttonBackground
target: controlBackground
color: control.style.background.disabled
border.color: control.style.border.disabled
}

View File

@@ -1,3 +1,4 @@
module StudioControls
AbstractButton 1.0 AbstractButton.qml
ActionIndicator 1.0 ActionIndicator.qml
Button 1.0 Button.qml

View File

@@ -1,3 +1,4 @@
module StudioTheme
singleton Values 1.0 Values.qml
singleton Constants 1.0 Constants.qml
ControlStyle 1.0 ControlStyle.qml

View File

@@ -0,0 +1,54 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import StudioControls as StudioControls
import StudioTheme as StudioTheme
import ScriptEditorBackend
StudioControls.TopLevelComboBox {
id: action
style: StudioTheme.Values.connectionPopupControlStyle
textRole: "text"
valueRole: "value"
property var backend
property int indexFromBackend: indexOfValue(backend.actionType)
onIndexFromBackendChanged: action.currentIndex = action.indexFromBackend
onActivated: backend.changeActionType(action.currentValue)
model: ListModel {
ListElement {
value: StatementDelegate.CallFunction
text: qsTr("Call Function")
enabled: true
}
ListElement {
value: StatementDelegate.Assign
text: qsTr("Assign")
enabled: true
}
ListElement {
value: StatementDelegate.ChangeState
text: qsTr("Change State")
enabled: true
}
ListElement {
value: StatementDelegate.SetProperty
text: qsTr("Set Property")
enabled: true
}
ListElement {
value: StatementDelegate.PrintMessage
text: qsTr("Print Message")
enabled: true
}
ListElement {
value: StatementDelegate.Custom
text: qsTr("Custom")
enabled: false
}
}
}

View File

@@ -4,13 +4,16 @@
import QtQuick
import StudioControls as StudioControls
import StudioTheme as StudioTheme
import ScriptEditorBackend
Rectangle {
id: root
property StudioTheme.ControlStyle style: StudioTheme.Values.controlStyle
property var conditionListModel: ConnectionsEditorEditorBackend.connectionModel.delegate.conditionListModel
property var conditionListModel: ScriptEditorBackend.conditionListModel // connect a valid model here
property alias popupListModel: popup.listModel
property alias popupTreeModel: popup.treeModel
property alias model: repeater.model
property int shadowPillIndex: -1

View File

@@ -5,6 +5,7 @@ import QtQuick
import StudioControls as StudioControls
import StudioTheme as StudioTheme
import HelperWidgets as HelperWidgets
import ScriptEditorBackend
FocusScope {
id: root

View File

@@ -0,0 +1,262 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import QtQuick.Controls
import HelperWidgets as HelperWidgets
import StudioTheme as StudioTheme
import ScriptEditorBackend
Column {
id: root
property real horizontalSpacing
property real verticalSpacing
property real columnWidth
property var backend
property alias keepOpen: expressionDialogLoader.visible
property int currentAction: StatementDelegate.Custom
property string itemTooltip: qsTr("Sets the component that is affected by the action.")
property string methodTooltip: qsTr("Sets the item component's method.")
property string fromTooltip: qsTr("Sets the component and its property from which the value is copied.")
property string toTooltip: qsTr("Sets the component and its property to which the copied value is assigned.")
property string addConditionTooltip: qsTr("Sets a logical condition for the selected action.")
property string removeConditionTooltip: qsTr("Removes the logical condition for the action.")
property string stateGroupTooltip: qsTr("Sets a <b>State Group</b> that is accessed when the action is initiated.")
property string stateTooltip: qsTr("Sets a <b>State</b> within the assigned <b>State Group</b> that is accessed when the action is initiated.")
property string propertyTooltip: qsTr("Sets the property of the component that is affected by the action.")
property string valueTooltip: qsTr("Sets the value of the property of the component that is affected by the action.")
property string messageTooltip: qsTr("Sets a text that is printed when the action is initiated.")
StatementEditor {
width: root.width
actionType: root.currentAction
horizontalSpacing: root.horizontalSpacing
columnWidth: root.columnWidth
statement: root.backend.okStatement
backend: root.backend
spacing: root.verticalSpacing
itemTooltip: root.itemTooltip
methodTooltip: root.methodTooltip
fromTooltip: root.fromTooltip
toTooltip: root.toTooltip
stateGroupTooltip: root.stateGroupTooltip
stateTooltip: root.stateTooltip
propertyTooltip: root.propertyTooltip
valueTooltip: root.valueTooltip
messageTooltip: root.messageTooltip
}
HelperWidgets.AbstractButton {
id: addConditionButton
style: StudioTheme.Values.connectionPopupButtonStyle
width: 160
buttonIcon: qsTr("Add Condition")
tooltip: root.addConditionTooltip
iconSize: StudioTheme.Values.baseFontSize
iconFontFamily: StudioTheme.Constants.font.family
anchors.horizontalCenter: parent.horizontalCenter
visible: root.currentAction !== StatementDelegate.Custom && !backend.hasCondition
onClicked: backend.addCondition()
}
HelperWidgets.AbstractButton {
style: StudioTheme.Values.connectionPopupButtonStyle
width: 160
buttonIcon: qsTr("Remove Condition")
tooltip: root.removeConditionTooltip
iconSize: StudioTheme.Values.baseFontSize
iconFontFamily: StudioTheme.Constants.font.family
anchors.horizontalCenter: parent.horizontalCenter
visible: root.currentAction !== StatementDelegate.Custom && backend.hasCondition
onClicked: backend.removeCondition()
}
ExpressionBuilder {
style: StudioTheme.Values.connectionPopupControlStyle
width: root.width
visible: backend.hasCondition
model: backend.conditionListModel
conditionListModel: backend.conditionListModel
popupListModel: backend.propertyListProxyModel
popupTreeModel: backend.propertyTreeModel
onRemove: function(index) {
backend.conditionListModel.removeToken(index)
}
onUpdate: function(index, value) {
backend.conditionListModel.updateToken(index, value)
}
onAdd: function(value) {
backend.conditionListModel.appendToken(value)
}
onInsert: function(index, value, type) {
if (type === ConditionListModel.Intermediate)
backend.conditionListModel.insertIntermediateToken(index, value)
else if (type === ConditionListModel.Shadow)
backend.conditionListModel.insertShadowToken(index, value)
else
backend.conditionListModel.insertToken(index, value)
}
onSetValue: function(index, value) {
backend.conditionListModel.setShadowToken(index, value)
}
}
HelperWidgets.AbstractButton {
style: StudioTheme.Values.connectionPopupButtonStyle
width: 160
buttonIcon: qsTr("Add Else Statement")
tooltip: qsTr("Sets an alternate condition for the previously defined logical condition.")
iconSize: StudioTheme.Values.baseFontSize
iconFontFamily: StudioTheme.Constants.font.family
anchors.horizontalCenter: parent.horizontalCenter
visible: root.currentAcion !== StatementDelegate.Custom
&& backend.hasCondition && !backend.hasElse
onClicked: backend.addElse()
}
HelperWidgets.AbstractButton {
style: StudioTheme.Values.connectionPopupButtonStyle
width: 160
buttonIcon: qsTr("Remove Else Statement")
tooltip: qsTr("Removes the alternate logical condition for the previously defined logical condition.")
iconSize: StudioTheme.Values.baseFontSize
iconFontFamily: StudioTheme.Constants.font.family
anchors.horizontalCenter: parent.horizontalCenter
visible: root.currentAction !== StatementDelegate.Custom
&& backend.hasCondition && backend.hasElse
onClicked: backend.removeElse()
}
//Else Statement
StatementEditor {
width: root.width
actionType: root.currentAction
horizontalSpacing: root.horizontalSpacing
columnWidth: root.columnWidth
statement: backend.koStatement
backend: root.backend
spacing: root.verticalSpacing
visible: action.currentValue !== StatementDelegate.Custom
&& backend.hasCondition && backend.hasElse
itemTooltip: root.itemTooltip
methodTooltip: root.methodTooltip
fromTooltip: root.fromTooltip
toTooltip: root.toTooltip
stateGroupTooltip: root.stateGroupTooltip
stateTooltip: root.stateTooltip
propertyTooltip: root.propertyTooltip
valueTooltip: root.valueTooltip
messageTooltip: root.messageTooltip
}
// code preview toolbar
Column {
id: miniToolbarEditor
width: parent.width
spacing: -2
Rectangle {
id: miniToolbar
width: parent.width
height: editorButton.height + 2
radius: 4
z: -1
color: StudioTheme.Values.themeConnectionEditorMicroToolbar
Row {
spacing: 2
HelperWidgets.AbstractButton {
id: editorButton
style: StudioTheme.Values.microToolbarButtonStyle
buttonIcon: StudioTheme.Constants.codeEditor_medium
tooltip: qsTr("Write the conditions manually.")
onClicked: expressionDialogLoader.show()
}
HelperWidgets.AbstractButton {
id: jumpToCodeButton
style: StudioTheme.Values.microToolbarButtonStyle
buttonIcon: StudioTheme.Constants.jumpToCode_medium
tooltip: qsTr("Jump to the code.")
onClicked: backend.jumpToCode()
}
}
}
// Editor
Rectangle {
id: editor
width: parent.width
height: 150
color: StudioTheme.Values.themeConnectionCodeEditor
Text {
id: code
anchors.fill: parent
anchors.margins: 4
text: backend.indentedSource
color: StudioTheme.Values.themeTextColor
font.pixelSize: StudioTheme.Values.myFontSize
wrapMode: Text.Wrap
horizontalAlignment: code.lineCount === 1 ? Text.AlignHCenter : Text.AlignLeft
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
Loader {
id: expressionDialogLoader
parent: editor
anchors.fill: parent
visible: false
active: visible
function show() {
expressionDialogLoader.visible = true
}
sourceComponent: Item {
id: bindingEditorParent
Component.onCompleted: {
bindingEditor.showWidget()
bindingEditor.text = backend.source
bindingEditor.showControls(false)
bindingEditor.setMultilne(true)
bindingEditor.updateWindowName()
}
HelperWidgets.ActionEditor {
id: bindingEditor
onRejected: {
bindingEditor.hideWidget()
expressionDialogLoader.visible = false
}
onAccepted: {
backend.setNewSource(bindingEditor.text)
bindingEditor.hideWidget()
expressionDialogLoader.visible = false
}
}
}
}
}
}
}

View File

@@ -6,7 +6,7 @@ import QtQuick.Controls
import HelperWidgets as HelperWidgets
import StudioControls as StudioControls
import StudioTheme as StudioTheme
import ConnectionsEditorEditorBackend
import ScriptEditorBackend
Column {
id: root
@@ -20,26 +20,37 @@ Column {
property var backend
property string itemTooltip
property alias methodTooltip: methodPopupLabel.tooltip
property alias fromTooltip: fromPopupLabel.tooltip
property alias toTooltip: toPopupLabel.tooltip
property alias stateGroupTooltip: stateGroupPopupLabel.tooltip
property alias stateTooltip: statePopupLabel.tooltip
property alias propertyTooltip: propertyPopupLabel.tooltip
property alias valueTooltip: valuePopupLabel.tooltip
property alias messageTooltip: messagePopupLabel.tooltip
// Call Function
Row {
visible: root.actionType === ConnectionModelStatementDelegate.CallFunction
visible: root.actionType === StatementDelegate.CallFunction
spacing: root.horizontalSpacing
PopupLabel {
HelperWidgets.PopupLabel {
id: itemPopupLabel
width: root.columnWidth
text: qsTr("Item")
tooltip: qsTr("Sets the component that is affected by the action of the <b>Target</b> component's <b>Signal</b>.")
tooltip: root.itemTooltip
}
PopupLabel {
HelperWidgets.PopupLabel {
id: methodPopupLabel
width: root.columnWidth
text: qsTr("Method")
tooltip: qsTr("Sets the item component's method that is affected by the <b>Target</b> component's <b>Signal</b>.")
}
}
Row {
visible: root.actionType === ConnectionModelStatementDelegate.CallFunction
visible: root.actionType === StatementDelegate.CallFunction
spacing: root.horizontalSpacing
StudioControls.TopLevelComboBox {
@@ -68,23 +79,23 @@ Column {
// Assign
Row {
visible: root.actionType === ConnectionModelStatementDelegate.Assign
visible: root.actionType === StatementDelegate.Assign
spacing: root.horizontalSpacing
PopupLabel {
HelperWidgets.PopupLabel {
id: fromPopupLabel
width: root.columnWidth
text: qsTr("From")
tooltip: qsTr("Sets the component and its property from which the value is copied when the <b>Target</b> component initiates the <b>Signal</b>.")
}
PopupLabel {
HelperWidgets.PopupLabel {
id: toPopupLabel
width: root.columnWidth
text: qsTr("To")
tooltip: qsTr("Sets the component and its property to which the copied value is assigned when the <b>Target</b> component initiates the <b>Signal</b>.")
}
}
Row {
visible: root.actionType === ConnectionModelStatementDelegate.Assign
visible: root.actionType === StatementDelegate.Assign
spacing: root.horizontalSpacing
StudioControls.TopLevelComboBox {
@@ -114,7 +125,7 @@ Column {
}
Row {
visible: root.actionType === ConnectionModelStatementDelegate.Assign
visible: root.actionType === StatementDelegate.Assign
spacing: root.horizontalSpacing
StudioControls.TopLevelComboBox {
@@ -145,24 +156,24 @@ Column {
// Change State
Row {
visible: root.actionType === ConnectionModelStatementDelegate.ChangeState
visible: root.actionType === StatementDelegate.ChangeState
spacing: root.horizontalSpacing
PopupLabel {
HelperWidgets.PopupLabel {
id: stateGroupPopupLabel
width: root.columnWidth
text: qsTr("State Group")
tooltip: qsTr("Sets a <b>State Group</b> that is accessed when the <b>Target</b> component initiates the <b>Signal</b>.")
}
PopupLabel {
HelperWidgets.PopupLabel {
id: statePopupLabel
width: root.columnWidth
text: qsTr("State")
tooltip: qsTr("Sets a <b>State</b> within the assigned <b>State Group</b> that is accessed when the <b>Target</b> component initiates the <b>Signal</b>.")
}
}
Row {
visible: root.actionType === ConnectionModelStatementDelegate.ChangeState
visible: root.actionType === StatementDelegate.ChangeState
spacing: root.horizontalSpacing
StudioControls.TopLevelComboBox {
@@ -191,24 +202,24 @@ Column {
// Set Property
Row {
visible: root.actionType === ConnectionModelStatementDelegate.SetProperty
visible: root.actionType === StatementDelegate.SetProperty
spacing: root.horizontalSpacing
PopupLabel {
HelperWidgets.PopupLabel {
width: root.columnWidth
text: qsTr("Item")
tooltip: qsTr("Sets the component that is affected by the action of the <b>Target</b> component's <b>Signal</b>.")
tooltip: root.itemTooltip
}
PopupLabel {
HelperWidgets.PopupLabel {
id: propertyPopupLabel
width: root.columnWidth
text: qsTr("Property")
tooltip: qsTr("Sets the property of the component that is affected by the action of the <b>Target</b> component's <b>Signal</b>.")
}
}
Row {
visible: root.actionType === ConnectionModelStatementDelegate.SetProperty
visible: root.actionType === StatementDelegate.SetProperty
spacing: root.horizontalSpacing
StudioControls.TopLevelComboBox {
@@ -236,16 +247,16 @@ Column {
}
}
PopupLabel {
HelperWidgets.PopupLabel {
id: valuePopupLabel
width: root.columnWidth
visible: root.actionType === ConnectionModelStatementDelegate.SetProperty
visible: root.actionType === StatementDelegate.SetProperty
text: qsTr("Value")
tooltip: qsTr("Sets the value of the property of the component that is affected by the action of the <b>Target</b> component's <b>Signal</b>.")
}
StudioControls.TextField {
id: setPropertyArgument
visible: root.actionType === ConnectionModelStatementDelegate.SetProperty
visible: root.actionType === StatementDelegate.SetProperty
width: root.width
actionIndicatorVisible: false
translationIndicatorVisible: false
@@ -257,16 +268,16 @@ Column {
}
// Print Message
PopupLabel {
HelperWidgets.PopupLabel {
id: messagePopupLabel
width: root.columnWidth
visible: root.actionType === ConnectionModelStatementDelegate.PrintMessage
visible: root.actionType === StatementDelegate.PrintMessage
text: qsTr("Message")
tooltip: qsTr("Sets a text that is printed when the <b>Signal</b> of the <b>Target</b> component initiates.")
}
StudioControls.TextField {
id: messageString
visible: root.actionType === ConnectionModelStatementDelegate.PrintMessage
visible: root.actionType === StatementDelegate.PrintMessage
width: root.width
actionIndicatorVisible: false
translationIndicatorVisible: false
@@ -277,8 +288,8 @@ Column {
}
// Custom
PopupLabel {
visible: root.actionType === ConnectionModelStatementDelegate.Custom
HelperWidgets.PopupLabel {
visible: root.actionType === StatementDelegate.Custom
text: qsTr("Custom Connections can only be edited with the binding editor")
width: root.width
horizontalAlignment: Text.AlignHCenter

View File

@@ -6,15 +6,15 @@ import QtQuick.Controls as Controls
import HelperWidgets as HelperWidgets
import StudioTheme as StudioTheme
import StudioControls as StudioControls
import ConnectionsEditorEditorBackend
import ScriptEditorBackend
Controls.Popup {
id: root
property StudioTheme.ControlStyle style: StudioTheme.Values.controlStyle
property var listModel: ConnectionsEditorEditorBackend.connectionModel.delegate.propertyListProxyModel
property var treeModel: ConnectionsEditorEditorBackend.connectionModel.delegate.propertyTreeModel
property var listModel: ScriptEditorBackend.propertyListProxyModel // connect a valid model here
property var treeModel: ScriptEditorBackend.propertyTreeModel // connect a valid model here
signal select(var value)
signal entered(var value)

View File

@@ -0,0 +1,2 @@
ScriptEditorForm 1.0 ScriptEditorForm.qml
ActionsComboBox 1.0 ActionsComboBox.qml

View File

@@ -26,8 +26,8 @@
import QtQuick
import QtQuick.Controls.Basic as Basic
import StatesEditor
import HelperWidgets 2.0 as HelperWidgets
import StudioControls 1.0 as StudioControls
import HelperWidgets as HelperWidgets
import StudioControls as StudioControls
import StudioTheme as StudioTheme
import StatesEditorBackend
@@ -313,7 +313,7 @@ Rectangle {
StudioControls.Dialog {
id: editDialog
title: qsTr("Rename state group")
standardButtons: Dialog.Apply | Dialog.Cancel
standardButtons: Basic.Dialog.Apply | Basic.Dialog.Cancel
x: editButton.x - Math.max(0, editButton.x + editDialog.width - root.width)
y: toolBar.height
width: Math.min(300, root.width)
@@ -328,15 +328,11 @@ Rectangle {
anchors.fill: parent
onTextChanged: {
let btn = editDialog.standardButton(Dialog.Apply)
let btn = editDialog.standardButton(Basic.Dialog.Apply)
if (!btn)
return
if (editDialog.previousString !== editTextField.text) {
btn.enabled = true
} else {
btn.enabled = false
}
btn.enabled = (editDialog.previousString !== editTextField.text)
}
onAccepted: editDialog.accept()
@@ -355,7 +351,7 @@ Rectangle {
editTextField.text = StatesEditorBackend.statesEditorModel.activeStateGroup
editDialog.previousString = StatesEditorBackend.statesEditorModel.activeStateGroup
let btn = editDialog.standardButton(Dialog.Apply)
let btn = editDialog.standardButton(Basic.Dialog.Apply)
btn.enabled = false
}
}

View File

@@ -526,6 +526,10 @@ protected:
{
out("pragma ", ast->pragmaToken);
out(ast->name.toString());
if (!ast->value.isEmpty()) {
out(": ");
out(ast->value.toString());
}
newLine();
return false;
}

View File

@@ -36,6 +36,36 @@ void insertUpdateDelete(SqliteRange &&sqliteRange,
auto endValueIterator = values.end();
auto lastValueIterator = endValueIterator;
auto doUpdate = [&](const auto &sqliteValue, const auto &value) {
UpdateChange updateChange = updateCallback(sqliteValue, value);
switch (updateChange) {
case UpdateChange::Update:
lastValueIterator = currentValueIterator;
break;
case UpdateChange::No:
lastValueIterator = endValueIterator;
break;
}
++currentSqliteIterator;
++currentValueIterator;
};
auto doInsert = [&](const auto &value) {
insertCallback(value);
++currentValueIterator;
};
auto doDelete = [&](const auto &sqliteValue) {
if (lastValueIterator != endValueIterator) {
if (compareKey(sqliteValue, *lastValueIterator) != 0)
deleteCallback(sqliteValue);
lastValueIterator = endValueIterator;
} else {
deleteCallback(sqliteValue);
}
++currentSqliteIterator;
};
while (true) {
bool hasMoreValues = currentValueIterator != endValueIterator;
bool hasMoreSqliteValues = currentSqliteIterator != endSqliteIterator;
@@ -43,44 +73,16 @@ void insertUpdateDelete(SqliteRange &&sqliteRange,
auto &&sqliteValue = *currentSqliteIterator;
auto &&value = *currentValueIterator;
auto compare = compareKey(sqliteValue, value);
if (compare == 0) {
UpdateChange updateChange = updateCallback(sqliteValue, value);
switch (updateChange) {
case UpdateChange::Update:
lastValueIterator = currentValueIterator;
break;
case UpdateChange::No:
lastValueIterator = endValueIterator;
break;
}
++currentSqliteIterator;
++currentValueIterator;
} else if (compare > 0) {
insertCallback(value);
++currentValueIterator;
} else if (compare < 0) {
if (lastValueIterator != endValueIterator) {
if (compareKey(sqliteValue, *lastValueIterator) != 0)
deleteCallback(sqliteValue);
lastValueIterator = endValueIterator;
} else {
deleteCallback(sqliteValue);
}
++currentSqliteIterator;
}
if (compare == 0)
doUpdate(sqliteValue, value);
else if (compare > 0)
doInsert(value);
else if (compare < 0)
doDelete(sqliteValue);
} else if (hasMoreValues) {
insertCallback(*currentValueIterator);
++currentValueIterator;
doInsert(*currentValueIterator);
} else if (hasMoreSqliteValues) {
auto &&sqliteValue = *currentSqliteIterator;
if (lastValueIterator != endValueIterator) {
if (compareKey(sqliteValue, *lastValueIterator) != 0)
deleteCallback(sqliteValue);
lastValueIterator = endValueIterator;
} else {
deleteCallback(sqliteValue);
}
++currentSqliteIterator;
doDelete(*currentSqliteIterator);
} else {
break;
}

View File

@@ -191,7 +191,7 @@ void SqlStatementBuilder::clearSqlStatement()
void SqlStatementBuilder::checkIfPlaceHolderExists(Utils::SmallStringView name) const
{
if (name.size() < 2 || !name.startsWith('$') || !m_sqlTemplate.contains(name))
if (name.size() < 2 || !name.starts_with('$') || !m_sqlTemplate.contains(name))
throwException("SqlStatementBuilder::bind: placeholder name does not exist!", name.data());
}

View File

@@ -112,13 +112,13 @@ public:
~BasicSmallString() noexcept
{
if (Q_UNLIKELY(hasAllocatedMemory()))
if (hasAllocatedMemory()) [[unlikely]]
Memory::deallocate(m_data.data());
}
BasicSmallString(const BasicSmallString &other) noexcept
{
if (Q_LIKELY(other.isShortString() || other.isReadOnlyReference()))
if (other.isShortString() || other.isReadOnlyReference()) [[likely]]
m_data = other.m_data;
else
new (this) BasicSmallString{other.data(), other.size()};
@@ -126,10 +126,10 @@ public:
BasicSmallString &operator=(const BasicSmallString &other) noexcept
{
if (Q_LIKELY(this != &other)) {
if (this != &other) [[likely]] {
this->~BasicSmallString();
if (Q_LIKELY(other.isShortString() || other.isReadOnlyReference()))
if (other.isShortString() || other.isReadOnlyReference()) [[likely]]
m_data = other.m_data;
else
new (this) BasicSmallString{other.data(), other.size()};
@@ -146,7 +146,7 @@ public:
BasicSmallString &operator=(BasicSmallString &&other) noexcept
{
if (Q_LIKELY(this != &other)) {
if (this != &other) [[likely]] {
this->~BasicSmallString();
m_data = std::move(other.m_data);
@@ -174,7 +174,7 @@ public:
SmallStringView toStringView() const noexcept { return SmallStringView(data(), size()); }
operator SmallStringView() const noexcept { return SmallStringView(data(), size()); }
constexpr operator SmallStringView() const noexcept { return SmallStringView(data(), size()); }
explicit operator QLatin1StringView() const noexcept
{
@@ -202,7 +202,7 @@ public:
void reserve(size_type newCapacity) noexcept
{
if (fitsNotInCapacity(newCapacity)) {
if (Q_UNLIKELY(hasAllocatedMemory())) {
if (hasAllocatedMemory()) {
m_data.setPointer(Memory::reallocate(m_data.data(), newCapacity));
m_data.setAllocatedCapacity(newCapacity);
} else {

View File

@@ -13,12 +13,6 @@
#include <string>
#include <string_view>
#if __cpp_lib_constexpr_string >= 201907L
#define constexpr_string constexpr
#else
#define constexpr_string
#endif
namespace Utils {
template <typename String>
@@ -36,41 +30,14 @@ class SmallStringView : public std::string_view
public:
using std::string_view::string_view;
constexpr SmallStringView(const_iterator begin, const_iterator end) noexcept
: std::string_view{std::addressof(*begin), static_cast<std::size_t>(std::distance(begin, end))}
{}
#ifdef Q_CC_MSVC
constexpr SmallStringView(const char *const begin, const char *const end) noexcept
: std::string_view{begin, static_cast<std::size_t>(std::distance(begin, end))}
{}
#endif
template<typename String, typename Utils::enable_if_has_char_data_pointer<String> = 0>
constexpr SmallStringView(const String &string) noexcept
: std::string_view{string.data(), static_cast<std::size_t>(string.size())}
{}
static constexpr SmallStringView fromUtf8(const char *const characterPointer)
{
return SmallStringView(characterPointer);
}
constexpr size_type isEmpty() const noexcept { return empty(); }
constexpr
SmallStringView mid(size_type position) const noexcept
{
return SmallStringView(data() + position, size() - position);
}
constexpr
SmallStringView mid(size_type position, size_type length) const noexcept
{
return SmallStringView(data() + position, length);
}
constexpr_string operator std::string() const { return std::string(data(), size()); }
operator std::string() const { return std::string(data(), size()); }
explicit operator QString() const { return QString::fromUtf8(data(), int(size())); }
@@ -89,57 +56,18 @@ public:
{
return QUtf8StringView(data(), Utils::ssize(*this));
}
constexpr bool startsWith(SmallStringView subStringToSearch) const noexcept
{
if (size() >= subStringToSearch.size())
return !std::char_traits<char>::compare(data(),
subStringToSearch.data(),
subStringToSearch.size());
return false;
}
constexpr bool startsWith(char characterToSearch) const noexcept
{
return *begin() == characterToSearch;
}
constexpr bool endsWith(SmallStringView ending) const noexcept
{
return size() >= ending.size() && std::equal(ending.rbegin(), ending.rend(), rbegin());
}
};
constexpr bool operator!=(SmallStringView first, SmallStringView second) noexcept
inline constexpr auto operator<=>(const SmallStringView &first, const SmallStringView &second)
{
return std::string_view{first} != std::string_view{second};
return std::string_view{first} <=> std::string_view{second};
}
constexpr bool operator==(SmallStringView first, SmallStringView second) noexcept
inline constexpr bool operator==(const SmallStringView &first, const SmallStringView &second)
{
return std::string_view{first} == std::string_view{second};
}
constexpr bool operator<(SmallStringView first, SmallStringView second) noexcept
{
return std::string_view{first} < std::string_view{second};
}
constexpr bool operator>(SmallStringView first, SmallStringView second) noexcept
{
return std::string_view{first} > std::string_view{second};
}
constexpr bool operator<=(SmallStringView first, SmallStringView second) noexcept
{
return std::string_view{first} <= std::string_view{second};
}
constexpr bool operator>=(SmallStringView first, SmallStringView second) noexcept
{
return std::string_view{first} >= std::string_view{second};
}
constexpr int compare(SmallStringView first, SmallStringView second) noexcept
{
return first.compare(second);

View File

@@ -103,7 +103,6 @@ void InsightWidget::reloadQmlSource()
{
QString statesListQmlFilePath = qmlSourcesPath() + QStringLiteral("/Main.qml");
QTC_ASSERT(QFileInfo::exists(statesListQmlFilePath), return );
engine()->clearComponentCache();
setSource(QUrl::fromLocalFile(statesListQmlFilePath));
if (!rootObject()) {

View File

@@ -100,6 +100,11 @@ DeployMcuProcessStep::DeployMcuProcessStep(ProjectExplorer::BuildStepList *bc, I
cmdLine.addArg(directory);
return cmdLine;
});
connect(this, &BuildStep::addOutput, this, [](const QString &str, OutputFormat fmt) {
if (fmt == OutputFormat::ErrorMessage)
showError(str);
});
}
// Workaround for QDS-13763, when UL-10456 is completed this can be removed with the next LTS

View File

@@ -104,7 +104,6 @@ add_qtc_plugin(QmlDesigner
qmldesignericons.h
qmldesignerplugin.cpp qmldesignerplugin.h
qmldesignerexternaldependencies.cpp qmldesignerexternaldependencies.h
qmldesignerprojectmanager.cpp qmldesignerprojectmanager.h
settingspage.cpp settingspage.h
shortcutmanager.cpp shortcutmanager.h
designermcumanager.cpp designermcumanager.h
@@ -133,6 +132,16 @@ if (QTC_STATIC_BUILD AND TARGET QmlDesigner)
extend_qtc_target(QmlDesigner PUBLIC_DEPENDS TextEditor)
endif()
extend_qtc_plugin(QmlDesigner
PUBLIC_INCLUDES project
SOURCES_PREFIX project
SOURCES
qmldesignerprojectmanager.cpp
qmldesignerprojectmanager.h
projectstorageerrornotifier.cpp
projectstorageerrornotifier.h
)
extend_qtc_plugin(QmlDesigner
PUBLIC_INCLUDES instances
SOURCES_PREFIX instances
@@ -412,7 +421,9 @@ extend_qtc_plugin(QmlDesigner
gradientpresetdefaultlistmodel.cpp gradientpresetdefaultlistmodel.h
gradientpresetitem.cpp gradientpresetitem.h
gradientpresetlistmodel.cpp gradientpresetlistmodel.h
instanceimageprovider.cpp instanceimageprovider.h
propertyeditorcontextobject.cpp propertyeditorcontextobject.h
propertyeditordynamicpropertiesproxymodel.cpp propertyeditordynamicpropertiesproxymodel.h
propertyeditorqmlbackend.cpp propertyeditorqmlbackend.h
propertyeditortransaction.cpp propertyeditortransaction.h
propertyeditorvalue.cpp propertyeditorvalue.h
@@ -421,7 +432,9 @@ extend_qtc_plugin(QmlDesigner
propertynamevalidator.cpp propertynamevalidator.h
tooltip.cpp tooltip.h
qmlanchorbindingproxy.cpp qmlanchorbindingproxy.h
qmlmaterialnodeproxy.cpp qmlmaterialnodeproxy.h
qmlmodelnodeproxy.cpp qmlmodelnodeproxy.h
qmltexturenodeproxy.cpp qmltexturenodeproxy.h
quick2propertyeditorview.cpp quick2propertyeditorview.h
propertyeditorutils.cpp propertyeditorutils.h
)
@@ -477,7 +490,6 @@ extend_qtc_plugin(QmlDesigner
materialbrowserwidget.cpp materialbrowserwidget.h
materialbrowsermodel.cpp materialbrowsermodel.h
materialbrowsertexturesmodel.cpp materialbrowsertexturesmodel.h
materialutils.cpp materialutils.h
)
extend_qtc_plugin(QmlDesigner
@@ -571,6 +583,16 @@ extend_qtc_plugin(QmlDesigner
annotationeditor.qrc
)
extend_qtc_plugin(QmlDesigner
SOURCES_PREFIX components/scripteditor
SOURCES
scripteditorstatements.cpp scripteditorstatements.h
scripteditorevaluator.cpp scripteditorevaluator.h
scripteditorutils.cpp scripteditorutils.h
propertytreemodel.cpp propertytreemodel.h
scripteditorbackend.cpp scripteditorbackend.h
)
extend_qtc_plugin(QmlDesigner
SOURCES_PREFIX components/connectioneditor
SOURCES
@@ -578,15 +600,12 @@ extend_qtc_plugin(QmlDesigner
bindingmodel.cpp bindingmodel.h
bindingmodelitem.cpp bindingmodelitem.h
connectioneditor.qrc
connectioneditorevaluator.cpp connectioneditorevaluator.h
connectioneditorstatements.cpp connectioneditorstatements.h
connectionmodel.cpp connectionmodel.h
connectionview.cpp connectionview.h
dynamicpropertiesmodel.cpp dynamicpropertiesmodel.h
dynamicpropertiesitem.cpp dynamicpropertiesitem.h
connectioneditorutils.cpp connectioneditorutils.h
selectiondynamicpropertiesproxymodel.cpp selectiondynamicpropertiesproxymodel.h
propertytreemodel.cpp propertytreemodel.h
connectioneditorlogging.cpp connectioneditorlogging.h
)
extend_qtc_plugin(QmlDesigner
@@ -780,39 +799,6 @@ extend_qtc_plugin(QmlDesigner
dvconnector.cpp dvconnector.h
)
add_qtc_plugin(assetexporterplugin
PLUGIN_CLASS AssetExporterPlugin
CONDITION TARGET QmlDesigner
PLUGIN_DEPENDS
Core ProjectExplorer QmlDesigner
DEPENDS Utils Qt::Qml Qt::QuickPrivate
PUBLIC_INCLUDES assetexporterplugin
PLUGIN_PATH ${QmlDesignerPluginInstallPrefix}
)
extend_qtc_plugin(assetexporterplugin
CONDITION ENABLE_COMPILE_WARNING_AS_ERROR
PROPERTIES COMPILE_WARNING_AS_ERROR ON
)
extend_qtc_plugin(assetexporterplugin
SOURCES_PREFIX assetexporterplugin
SOURCES
assetexportdialog.h assetexportdialog.cpp
assetexporter.h assetexporter.cpp
assetexporterplugin.h assetexporterplugin.cpp
assetexporterview.h assetexporterview.cpp
assetexportpluginconstants.h
componentexporter.h componentexporter.cpp
exportnotification.h exportnotification.cpp
filepathmodel.h filepathmodel.cpp
dumpers/assetnodedumper.h dumpers/assetnodedumper.cpp
dumpers/itemnodedumper.h dumpers/itemnodedumper.cpp
dumpers/nodedumper.h dumpers/nodedumper.cpp
dumpers/textnodedumper.h dumpers/textnodedumper.cpp
assetexporterplugin.qrc
)
add_qtc_plugin(componentsplugin
PLUGIN_CLASS ComponentsPlugin
CONDITION TARGET QmlDesigner

View File

@@ -1,228 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "assetexportdialog.h"
#include "../qmldesignertr.h"
#include "assetexportpluginconstants.h"
#include "filepathmodel.h"
#include <coreplugin/fileutils.h>
#include <coreplugin/icore.h>
#include <projectexplorer/task.h>
#include <projectexplorer/taskhub.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <utils/detailswidget.h>
#include <utils/layoutbuilder.h>
#include <utils/outputformatter.h>
#include <utils/pathchooser.h>
#include <QCheckBox>
#include <QPushButton>
#include <QListView>
#include <QPlainTextEdit>
#include <QDialogButtonBox>
#include <QScrollBar>
#include <QGridLayout>
#include <QProgressBar>
#include <QLabel>
#include <QStackedWidget>
#include <algorithm>
#include <cmath>
using namespace ProjectExplorer;
using namespace Utils;
namespace QmlDesigner {
static void addFormattedMessage(OutputFormatter *formatter, const QString &str, OutputFormat format)
{
if (!formatter)
return;
QPlainTextEdit *edit = formatter->plainTextEdit();
QScrollBar *scroll = edit->verticalScrollBar();
bool isAtBottom = scroll && scroll->value() == scroll->maximum();
QString msg = str + "\n";
formatter->appendMessage(msg, format);
if (isAtBottom)
scroll->setValue(scroll->maximum());
}
AssetExportDialog::AssetExportDialog(const FilePath &exportPath,
AssetExporter &assetExporter, FilePathModel &model,
QWidget *parent) :
QDialog(parent),
m_assetExporter(assetExporter),
m_filePathModel(model),
m_filesView(new QListView),
m_exportLogs(new QPlainTextEdit),
m_outputFormatter(new Utils::OutputFormatter())
{
resize(768, 480);
setWindowTitle(Tr::tr("Export Components"));
m_stackedWidget = new QStackedWidget;
m_exportProgress = new QProgressBar;
m_exportProgress->setRange(0,0);
auto optionsWidget = new QWidget;
auto advancedOptions = new DetailsWidget;
advancedOptions->setSummaryText(tr("Advanced Options"));
advancedOptions->setWidget(optionsWidget);
m_buttonBox = new QDialogButtonBox;
m_buttonBox->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Close);
m_exportPath = new PathChooser;
m_exportPath->setExpectedKind(PathChooser::Kind::SaveFile);
m_exportPath->setFilePath(
exportPath.pathAppended(
ProjectExplorer::ProjectManager::startupProject()->displayName() + ".metadata"
));
m_exportPath->setPromptDialogTitle(tr("Choose Export File"));
m_exportPath->setPromptDialogFilter(tr("Metadata file (*.metadata)"));
m_exportPath->lineEdit()->setReadOnly(true);
m_exportPath->addButton(tr("Open"), this, [this] {
Core::FileUtils::showInGraphicalShell(m_exportPath->filePath());
});
m_exportAssetsCheck = new QCheckBox(tr("Export assets"), this);
m_exportAssetsCheck->setChecked(true);
m_perComponentExportCheck = new QCheckBox(tr("Export components separately"), this);
m_perComponentExportCheck->setChecked(false);
m_buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false);
m_stackedWidget->addWidget(m_filesView);
m_filesView->setModel(&m_filePathModel);
m_exportLogs->setReadOnly(true);
m_outputFormatter->setPlainTextEdit(m_exportLogs);
m_stackedWidget->addWidget(m_exportLogs);
switchView(false);
connect(m_buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, [this] {
m_buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false);
m_assetExporter.cancel();
});
m_exportBtn = m_buttonBox->addButton(tr("Export"), QDialogButtonBox::AcceptRole);
m_exportBtn->setEnabled(false);
connect(m_exportBtn, &QPushButton::clicked, this, &AssetExportDialog::onExport);
connect(&m_filePathModel, &FilePathModel::modelReset, this, [this] {
m_exportProgress->setRange(0, 1000);
m_exportProgress->setValue(0);
m_exportBtn->setEnabled(true);
});
connect(m_buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, [this] {
close();
});
m_buttonBox->button(QDialogButtonBox::Close)->setVisible(false);
connect(&m_assetExporter, &AssetExporter::stateChanged,
this, &AssetExportDialog::onExportStateChanged);
connect(&m_assetExporter, &AssetExporter::exportProgressChanged,
this, &AssetExportDialog::updateExportProgress);
connect(&taskHub(), &TaskHub::taskAdded, this, &AssetExportDialog::onTaskAdded);
using namespace Layouting;
Column {
m_exportAssetsCheck,
m_perComponentExportCheck,
st,
noMargin,
}.attachTo(optionsWidget);
Column {
Form { Tr::tr("Export path:"), m_exportPath },
advancedOptions,
m_stackedWidget,
m_exportProgress,
m_buttonBox,
}.attachTo(this);
}
AssetExportDialog::~AssetExportDialog()
{
m_assetExporter.cancel();
}
void AssetExportDialog::onExport()
{
switchView(true);
updateExportProgress(0.0);
TaskHub::clearTasks(Constants::TASK_CATEGORY_ASSET_EXPORT);
m_exportLogs->clear();
Utils::FilePath selectedPath = m_exportPath->filePath();
Utils::FilePath exportPath = m_perComponentExportCheck->isChecked() ?
(selectedPath.isDir() ? selectedPath : selectedPath.parentDir()) :
selectedPath;
m_assetExporter.exportQml(m_filePathModel.files(), exportPath,
m_exportAssetsCheck->isChecked(),
m_perComponentExportCheck->isChecked());
}
void AssetExportDialog::onExportStateChanged(AssetExporter::ParsingState newState)
{
switch (newState) {
case AssetExporter::ParsingState::ExportingDone:
m_exportBtn->setVisible(false);
m_buttonBox->button(QDialogButtonBox::Close)->setVisible(true);
break;
default:
break;
}
m_exportBtn->setEnabled(newState == AssetExporter::ParsingState::ExportingDone);
m_buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(m_assetExporter.isBusy());
}
void AssetExportDialog::updateExportProgress(double value)
{
value = std::max(0.0, std::min(1.0, value));
m_exportProgress->setValue(std::round(value * 1000));
}
void AssetExportDialog::switchView(bool showExportView)
{
if (showExportView)
m_stackedWidget->setCurrentWidget(m_exportLogs);
else
m_stackedWidget->setCurrentWidget(m_filesView);
}
void AssetExportDialog::onTaskAdded(const ProjectExplorer::Task &task)
{
Utils::OutputFormat format = Utils::NormalMessageFormat;
if (task.category == Constants::TASK_CATEGORY_ASSET_EXPORT) {
switch (task.type) {
case ProjectExplorer::Task::Error:
format = Utils::StdErrFormat;
break;
case ProjectExplorer::Task::Warning:
format = Utils::StdOutFormat;
break;
default:
format = Utils::NormalMessageFormat;
}
addFormattedMessage(m_outputFormatter, task.description(), format);
}
}
}

View File

@@ -1,67 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#pragma once
#include "assetexporter.h"
#include <utils/filepath.h>
#include <QDialog>
#include <QStringListModel>
QT_BEGIN_NAMESPACE
class QPushButton;
class QCheckBox;
class QDialogButtonBox;
class QListView;
class QPlainTextEdit;
class QProgressBar;
class QStackedWidget;
QT_END_NAMESPACE
namespace Utils {
class OutputFormatter;
class PathChooser;
}
namespace ProjectExplorer {
class Task;
}
namespace QmlDesigner {
class FilePathModel;
class AssetExportDialog : public QDialog
{
Q_OBJECT
public:
explicit AssetExportDialog(const Utils::FilePath &exportPath, AssetExporter &assetExporter,
FilePathModel& model, QWidget *parent = nullptr);
~AssetExportDialog();
private:
void onExport();
void onExportStateChanged(AssetExporter::ParsingState newState);
void updateExportProgress(double value);
void switchView(bool showExportView);
void onTaskAdded(const ProjectExplorer::Task &task);
private:
AssetExporter &m_assetExporter;
FilePathModel &m_filePathModel;
QPushButton *m_exportBtn = nullptr;
QCheckBox *m_exportAssetsCheck = nullptr;
QCheckBox *m_perComponentExportCheck = nullptr;
QListView *m_filesView = nullptr;
QPlainTextEdit *m_exportLogs = nullptr;
Utils::OutputFormatter *m_outputFormatter = nullptr;
Utils::PathChooser *m_exportPath = nullptr;
QDialogButtonBox *m_buttonBox = nullptr;
QStackedWidget *m_stackedWidget = nullptr;
QProgressBar *m_exportProgress = nullptr;
};
} // QmlDesigner

View File

@@ -1,550 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "assetexporter.h"
#include "componentexporter.h"
#include "exportnotification.h"
#include <designdocument.h>
#include <modelutils.h>
#include <nodemetainfo.h>
#include <qmldesignerplugin.h>
#include <qmldesignertr.h>
#include <qmlitemnode.h>
#include <qmlobjectnode.h>
#include <rewriterview.h>
#include <coreplugin/editormanager/editormanager.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
#include <utils/async.h>
#include <utils/fileutils.h>
#include <utils/qtcassert.h>
#include <auxiliarydataproperties.h>
#include <QCryptographicHash>
#include <QDir>
#include <QJsonArray>
#include <QJsonDocument>
#include <QLoggingCategory>
#include <QPlainTextEdit>
#include <QWaitCondition>
#include <random>
#include <queue>
using namespace ProjectExplorer;
using namespace std;
namespace {
bool makeParentPath(const Utils::FilePath &path)
{
QDir d;
return d.mkpath(path.toFileInfo().absolutePath());
}
QByteArray generateHash(const QString &token) {
static uint counter = 0;
std::mt19937 gen(std::random_device().operator()());
std::uniform_int_distribution<> distribution(1, 99999);
QByteArray data = QString("%1%2%3").arg(token).arg(++counter).arg(distribution(gen)).toLatin1();
return QCryptographicHash::hash(data, QCryptographicHash::Md5).toHex();
}
Q_LOGGING_CATEGORY(loggerInfo, "qtc.designer.assetExportPlugin.assetExporter", QtInfoMsg)
Q_LOGGING_CATEGORY(loggerWarn, "qtc.designer.assetExportPlugin.assetExporter", QtWarningMsg)
Q_LOGGING_CATEGORY(loggerError, "qtc.designer.assetExportPlugin.assetExporter", QtCriticalMsg)
}
namespace QmlDesigner {
class AssetDumper
{
public:
AssetDumper();
~AssetDumper();
void dumpAsset(const QPixmap &p, const Utils::FilePath &path);
/* Keeps on dumping until all assets are dumped, then quits */
void quitDumper();
/* Aborts dumping */
void abortDumper();
private:
void addAsset(const QPixmap &p, const Utils::FilePath &path);
void doDumping(QPromise<void> &promise);
void savePixmap(const QPixmap &p, Utils::FilePath &path) const;
QFuture<void> m_dumpFuture;
QMutex m_queueMutex;
QWaitCondition m_queueCondition;
std::queue<std::pair<QPixmap, Utils::FilePath>> m_assets;
std::atomic<bool> m_quitDumper;
};
AssetExporter::AssetExporter(AssetExporterView *view,
ProjectExplorer::Project *project,
ProjectStorageDependencies projectStorageDependencies)
: m_currentState(*this)
, m_project(project)
, m_view(view)
, m_projectStorageDependencies{projectStorageDependencies}
{
connect(m_view, &AssetExporterView::loadingFinished, this, &AssetExporter::onQmlFileLoaded);
connect(m_view, &AssetExporterView::loadingError, this, &AssetExporter::notifyLoadError);
}
AssetExporter::~AssetExporter()
{
cancel();
}
void AssetExporter::exportQml(const Utils::FilePaths &qmlFiles, const Utils::FilePath &exportPath,
bool exportAssets, bool perComponentExport)
{
m_perComponentExport = perComponentExport;
ExportNotification::addInfo(tr("Export root directory: %1.\nExporting assets: %2")
.arg(exportPath.isDir()
? exportPath.toUserOutput()
: exportPath.parentDir().toUserOutput())
.arg(exportAssets? tr("Yes") : tr("No")));
if (m_perComponentExport)
ExportNotification::addInfo(tr("Each component is exported separately."));
notifyProgress(0.0);
m_exportFiles = qmlFiles;
m_totalFileCount = m_exportFiles.count();
m_components.clear();
m_componentUuidCache.clear();
m_exportPath = exportPath.isDir() ? exportPath : exportPath.parentDir();
m_exportFile = exportPath.fileName();
m_currentState.change(ParsingState::Parsing);
if (exportAssets)
m_assetDumper = make_unique<AssetDumper>();
else
m_assetDumper.reset();
QTimer::singleShot(0, this, &AssetExporter::beginExport);
}
void AssetExporter::beginExport()
{
for (const Utils::FilePath &p : std::as_const(m_exportFiles)) {
if (m_cancelled)
break;
preprocessQmlFile(p);
}
if (!m_cancelled)
triggerLoadNextFile();
}
void AssetExporter::cancel()
{
if (!m_cancelled) {
ExportNotification::addInfo(tr("Canceling export."));
m_assetDumper.reset();
m_cancelled = true;
}
}
bool AssetExporter::isBusy() const
{
return m_currentState == AssetExporter::ParsingState::Parsing ||
m_currentState == AssetExporter::ParsingState::ExportingAssets ||
m_currentState == AssetExporter::ParsingState::WritingJson;
}
const QPixmap &AssetExporter::generateAsset(const ModelNode &node)
{
static QPixmap nullPixmap;
if (m_cancelled)
return nullPixmap;
const QString uuid = node.auxiliaryDataWithDefault(uuidProperty).toString();
QTC_ASSERT(!uuid.isEmpty(), return nullPixmap);
if (!m_assets.contains(uuid)) {
// Generate asset.
QmlObjectNode objectNode(node);
QPixmap asset = objectNode.toQmlItemNode().instanceRenderPixmap();
m_assets[uuid] = asset;
}
return m_assets[uuid];
}
Utils::FilePath AssetExporter::assetPath(const ModelNode &node, const Component *component,
const QString &suffix) const
{
const QString uuid = node.auxiliaryDataWithDefault(uuidProperty).toString();
if (!component || uuid.isEmpty())
return {};
const Utils::FilePath assetExportDir =
m_perComponentExport ? componentExportDir(component) : m_exportPath;
const Utils::FilePath assetPath = assetExportDir.pathAppended("assets")
.pathAppended(uuid + suffix + ".png");
return assetPath;
}
void AssetExporter::exportAsset(const QPixmap &asset, const Utils::FilePath &path)
{
if (m_cancelled || !m_assetDumper)
return;
m_assetDumper->dumpAsset(asset, path);
}
void AssetExporter::exportComponent(const ModelNode &rootNode)
{
qCDebug(loggerInfo) << "Exporting component" << rootNode.id();
m_components.push_back(make_unique<Component>(*this, rootNode));
m_components.back()->exportComponent();
}
void AssetExporter::notifyLoadError(AssetExporterView::LoadState state)
{
QString errorStr = tr("Unknown error.");
switch (state) {
case AssetExporterView::LoadState::Exausted:
errorStr = tr("Loading file is taking too long.");
break;
case AssetExporterView::LoadState::QmlErrorState:
errorStr = tr("Cannot parse. The file contains coding errors.");
break;
default:
return;
}
qCDebug(loggerError) << "QML load error:" << errorStr;
ExportNotification::addError(tr("Loading components failed. %1").arg(errorStr));
}
void AssetExporter::notifyProgress(double value) const
{
emit exportProgressChanged(value);
}
void AssetExporter::onQmlFileLoaded()
{
QTC_ASSERT(m_view && m_view->model(), qCDebug(loggerError) << "Null model"; return);
qCDebug(loggerInfo) << "Qml file load done" << m_view->model()->fileUrl();
QmlDesigner::DesignDocument *designDocument = QmlDesigner::QmlDesignerPlugin::instance()
->documentManager()
.currentDesignDocument();
if (designDocument->hasQmlParseErrors()) {
ExportNotification::addError(tr("Cannot export component. Document \"%1\" has parsing errors.")
.arg(designDocument->displayName()));
} else {
exportComponent(m_view->rootModelNode());
if (Utils::Result res = m_view->saveQmlFile(); !res) {
ExportNotification::addError(tr("Error saving component file. %1")
.arg(res.error().isEmpty()? tr("Unknown") : res.error()));
}
}
notifyProgress((m_totalFileCount - m_exportFiles.count()) * 0.8 / m_totalFileCount);
triggerLoadNextFile();
}
Utils::FilePath AssetExporter::componentExportDir(const Component *component) const
{
return m_exportPath.pathAppended(component->name());
}
void AssetExporter::preprocessQmlFile(const Utils::FilePath &path)
{
// Load the QML file and assign UUIDs to items having none.
// Meanwhile cache the Component UUIDs as well
#ifdef QDS_USE_PROJECTSTORAGE
ModelPointer model = Model::create(m_projectStorageDependencies,
"Item",
{Import::createLibraryImport("QtQuick")},
path.path());
#else
ModelPointer model = Model::create("Item", 2, 7);
#endif
Utils::FileReader reader;
if (!reader.fetch(path)) {
ExportNotification::addError(tr("Cannot preprocess file: %1. Error %2")
.arg(path.toUserOutput()).arg(reader.errorString()));
return;
}
QPlainTextEdit textEdit;
textEdit.setPlainText(QString::fromUtf8(reader.data()));
NotIndentingTextEditModifier *modifier = new NotIndentingTextEditModifier(textEdit.document());
modifier->setParent(model.get());
auto rewriterView = std::make_unique<RewriterView>(m_view->externalDependencies(),
QmlDesigner::RewriterView::Validate);
rewriterView->setCheckSemanticErrors(false);
rewriterView->setTextModifier(modifier);
model->attachView(rewriterView.get());
rewriterView->restoreAuxiliaryData();
ModelNode rootNode = rewriterView->rootModelNode();
if (!rootNode.isValid()) {
ExportNotification::addError(tr("Cannot preprocess file: %1").arg(path.toUrlishString()));
return;
}
if (assignUuids(rootNode)) {
// Some UUIDs were assigned. Rewrite the file.
rewriterView->writeAuxiliaryData();
const QByteArray data = textEdit.toPlainText().toUtf8();
Utils::FileSaver saver(path, QIODevice::Text);
saver.write(data);
if (!saver.finalize()) {
ExportNotification::addError(tr("Cannot update %1.\n%2")
.arg(path.toUserOutput()).arg(saver.errorString()));
return;
}
// Close the document if already open.
// UUIDS are changed and editor must reopen the document, otherwise stale state of the
// document is loaded.
for (Core::IDocument *doc : Core::DocumentModel::openedDocuments()) {
if (doc->filePath() == path) {
Core::EditorManager::closeDocuments({doc}, false);
break;
}
}
}
// Cache component UUID
const QString uuid = rootNode.auxiliaryDataWithDefault(uuidProperty).toString();
m_componentUuidCache[path.toUrlishString()] = uuid;
}
bool AssetExporter::assignUuids(const ModelNode &root)
{
// Assign an UUID to the node without one.
// Return true if an assignment takes place.
bool changed = false;
for (const ModelNode &node : root.allSubModelNodesAndThisNode()) {
const QString uuid = node.auxiliaryDataWithDefault(uuidProperty).toString();
if (uuid.isEmpty()) {
// Assign an unique identifier to the node.
QByteArray uuid = generateUuid(node);
node.setAuxiliaryData(uuidProperty, QString::fromLatin1(uuid));
changed = true;
}
}
return changed;
}
QByteArray AssetExporter::generateUuid(const ModelNode &node)
{
QByteArray uuid;
do {
uuid = generateHash(node.id());
} while (m_usedHashes.contains(uuid));
m_usedHashes.insert(uuid);
return uuid;
}
QString AssetExporter::componentUuid(const ModelNode &instance) const
{
// Returns the UUID of the component's root node
// Empty string is returned if the node is not an instance of a component within
// the project.
if (instance) {
const QString path = ModelUtils::componentFilePath(instance);
return m_componentUuidCache.value(path);
}
return {};
}
void AssetExporter::triggerLoadNextFile()
{
QTimer::singleShot(0, this, &AssetExporter::loadNextFile);
}
void AssetExporter::loadNextFile()
{
if (m_cancelled || m_exportFiles.isEmpty()) {
notifyProgress(0.8);
m_currentState.change(ParsingState::ParsingFinished);
writeMetadata();
return;
}
// Load the next pending file.
const Utils::FilePath file = m_exportFiles.takeFirst();
ExportNotification::addInfo(tr("Exporting file %1.").arg(file.toUserOutput()));
qCDebug(loggerInfo) << "Loading next file" << file;
m_view->loadQmlFile(file);
}
void AssetExporter::writeMetadata() const
{
if (m_cancelled) {
notifyProgress(1.0);
ExportNotification::addInfo(tr("Export canceled."));
m_currentState.change(ParsingState::ExportingDone);
return;
}
auto writeFile = [](const Utils::FilePath &path, const QJsonArray &artboards) {
if (!makeParentPath(path)) {
ExportNotification::addError(tr("Writing metadata failed. Cannot create file %1").
arg(path.toUrlishString()));
return;
}
ExportNotification::addInfo(tr("Writing metadata to file %1.").arg(path.toUserOutput()));
QJsonObject jsonRoot; // TODO: Write plugin info to root
jsonRoot.insert("artboards", artboards);
QJsonDocument doc(jsonRoot);
if (doc.isNull() || doc.isEmpty()) {
ExportNotification::addError(tr("Empty JSON document."));
return;
}
Utils::FileSaver saver(path, QIODevice::Text);
saver.write(doc.toJson(QJsonDocument::Indented));
if (!saver.finalize()) {
ExportNotification::addError(tr("Writing metadata failed. %1").
arg(saver.errorString()));
}
};
m_currentState.change(ParsingState::WritingJson);
auto const startupProject = ProjectExplorer::ProjectManager::startupProject();
QTC_ASSERT(startupProject, return);
const QString projectName = startupProject->displayName();
if (m_perComponentExport) {
for (auto &component : m_components) {
const Utils::FilePath path = componentExportDir(component.get());
writeFile(path.pathAppended(component->name() + ".metadata"), {component->json()});
}
} else {
QJsonArray artboards;
std::transform(m_components.cbegin(), m_components.cend(), back_inserter(artboards),
[](const unique_ptr<Component> &c) {return c->json(); });
writeFile(m_exportPath.pathAppended(m_exportFile), artboards);
}
notifyProgress(1.0);
ExportNotification::addInfo(tr("Export finished."));
if (m_assetDumper)
m_assetDumper->quitDumper();
m_currentState.change(ParsingState::ExportingDone);
}
AssetExporter::State::State(AssetExporter &exporter) :
m_assetExporter(exporter)
{
}
void AssetExporter::State::change(const ParsingState &state)
{
qCDebug(loggerInfo()) << "Assetimporter State change: Old: " << m_state << "New: " << state;
if (m_state != state) {
m_state = state;
emit m_assetExporter.stateChanged(m_state);
}
}
QDebug operator<<(QDebug os, const AssetExporter::ParsingState &s)
{
os << static_cast<std::underlying_type<QmlDesigner::AssetExporter::ParsingState>::type>(s);
return os;
}
AssetDumper::AssetDumper():
m_quitDumper(false)
{
m_dumpFuture = Utils::asyncRun(&AssetDumper::doDumping, this);
}
AssetDumper::~AssetDumper()
{
abortDumper();
}
void AssetDumper::dumpAsset(const QPixmap &p, const Utils::FilePath &path)
{
addAsset(p, path);
}
void AssetDumper::quitDumper()
{
m_quitDumper = true;
m_queueCondition.wakeAll();
if (!m_dumpFuture.isFinished())
m_dumpFuture.waitForFinished();
}
void AssetDumper::abortDumper()
{
if (!m_dumpFuture.isFinished()) {
m_dumpFuture.cancel();
m_queueCondition.wakeAll();
m_dumpFuture.waitForFinished();
}
}
void AssetDumper::addAsset(const QPixmap &p, const Utils::FilePath &path)
{
QMutexLocker locker(&m_queueMutex);
qDebug() << "Save Asset:" << path;
m_assets.push({p, path});
}
void AssetDumper::doDumping(QPromise<void> &promise)
{
auto haveAsset = [this] (std::pair<QPixmap, Utils::FilePath> *asset) {
QMutexLocker locker(&m_queueMutex);
if (m_assets.empty())
return false;
*asset = m_assets.front();
m_assets.pop();
return true;
};
forever {
std::pair<QPixmap, Utils::FilePath> asset;
if (haveAsset(&asset)) {
if (promise.isCanceled())
break;
savePixmap(asset.first, asset.second);
} else {
if (m_quitDumper)
break;
QMutexLocker locker(&m_queueMutex);
m_queueCondition.wait(&m_queueMutex);
}
if (promise.isCanceled())
break;
}
}
void AssetDumper::savePixmap(const QPixmap &p, Utils::FilePath &path) const
{
if (p.isNull()) {
qCDebug(loggerWarn) << "Dumping null pixmap" << path;
return;
}
if (!makeParentPath(path)) {
ExportNotification::addError(Tr::tr("Error creating asset directory. %1").arg(path.fileName()));
return;
}
if (!p.save(path.toUrlishString())) {
ExportNotification::addError(Tr::tr("Error saving asset. %1").arg(path.fileName()));
}
}
}

View File

@@ -1,99 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#pragma once
#include "assetexporterview.h"
#include <QJsonObject>
#include <QSet>
#include <memory>
namespace ProjectExplorer { class Project; }
namespace QmlDesigner {
class AssetDumper;
class Component;
class AssetExporter : public QObject
{
Q_OBJECT
public:
enum class ParsingState {
Idle = 0,
Parsing,
ParsingFinished,
ExportingAssets,
ExportingAssetsFinished,
WritingJson,
ExportingDone
};
AssetExporter(AssetExporterView *view,
ProjectExplorer::Project *project,
ProjectStorageDependencies projectStorageDependencies);
~AssetExporter();
void exportQml(const Utils::FilePaths &qmlFiles, const Utils::FilePath &exportPath,
bool exportAssets, bool perComponentExport);
void cancel();
bool isBusy() const;
const QPixmap &generateAsset(const ModelNode &node);
Utils::FilePath assetPath(const ModelNode &node, const Component *component,
const QString &suffix = {}) const;
void exportAsset(const QPixmap &asset, const Utils::FilePath &path);
QByteArray generateUuid(const ModelNode &node);
QString componentUuid(const ModelNode &instance) const;
signals:
void stateChanged(ParsingState);
void exportProgressChanged(double) const;
private:
ParsingState currentState() const { return m_currentState.m_state; }
void exportComponent(const ModelNode &rootNode);
void writeMetadata() const;
void notifyLoadError(AssetExporterView::LoadState state);
void notifyProgress(double value) const;
void triggerLoadNextFile();
void loadNextFile();
void onQmlFileLoaded();
Utils::FilePath componentExportDir(const Component *component) const;
void beginExport();
void preprocessQmlFile(const Utils::FilePath &path);
bool assignUuids(const ModelNode &root);
private:
mutable class State {
public:
State(AssetExporter&);
void change(const ParsingState &state);
operator ParsingState() const { return m_state; }
AssetExporter &m_assetExporter;
ParsingState m_state = ParsingState::Idle;
} m_currentState;
ProjectExplorer::Project *m_project = nullptr;
AssetExporterView *m_view = nullptr;
Utils::FilePaths m_exportFiles;
unsigned int m_totalFileCount = 0;
Utils::FilePath m_exportPath;
QString m_exportFile;
bool m_perComponentExport = false;
std::vector<std::unique_ptr<Component>> m_components;
QHash<QString, QString> m_componentUuidCache;
QSet<QByteArray> m_usedHashes;
QHash<QString, QPixmap> m_assets;
ProjectStorageDependencies m_projectStorageDependencies;
std::unique_ptr<AssetDumper> m_assetDumper;
bool m_cancelled = false;
};
QDebug operator<< (QDebug os, const QmlDesigner::AssetExporter::ParsingState& s);
} // namespace QmlDesigner

View File

@@ -1,115 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "assetexporterplugin.h"
#include "assetexportdialog.h"
#include "assetexporter.h"
#include "assetexporterview.h"
#include "assetexportpluginconstants.h"
#include "componentexporter.h"
#include "filepathmodel.h"
#include <qmldesignerprojectmanager.h>
#include "dumpers/itemnodedumper.h"
#include "dumpers/textnodedumper.h"
#include "dumpers/assetnodedumper.h"
#include "coreplugin/actionmanager/actionmanager.h"
#include "coreplugin/actionmanager/actioncontainer.h"
#include "coreplugin/documentmanager.h"
#include "qmldesigner/qmldesignerplugin.h"
#include "projectexplorer/projectexplorerconstants.h"
#include "projectexplorer/projectmanager.h"
#include "projectexplorer/project.h"
#include "projectexplorer/projectmanager.h"
#include "projectexplorer/taskhub.h"
#include "extensionsystem/pluginmanager.h"
#include "extensionsystem/pluginspec.h"
#include "utils/algorithm.h"
#include <QCoreApplication>
#include <QAction>
#include <QLoggingCategory>
namespace QmlDesigner {
AssetExporterPlugin::AssetExporterPlugin()
: m_projectManager{QmlDesigner::QmlDesignerPlugin::projectManagerForPluginInitializationOnly()}
{
ProjectExplorer::TaskHub::addCategory({Constants::TASK_CATEGORY_ASSET_EXPORT,
tr("Asset Export"),
tr("Issues with exporting assets."),
false});
auto *designerPlugin = QmlDesigner::QmlDesignerPlugin::instance();
auto &viewManager = designerPlugin->viewManager();
m_view = viewManager.registerView(std::make_unique<AssetExporterView>(
designerPlugin->externalDependenciesForPluginInitializationOnly()));
// Add dumper templates for factory instantiation.
Component::addNodeDumper<ItemNodeDumper>();
Component::addNodeDumper<TextNodeDumper>();
Component::addNodeDumper<AssetNodeDumper>();
// Instantiate actions created by the plugin.
addActions();
connect(ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::startupProjectChanged,
this, &AssetExporterPlugin::updateActions);
updateActions();
}
QString AssetExporterPlugin::pluginName() const
{
return QLatin1String("AssetExporterPlugin");
}
void AssetExporterPlugin::onExport()
{
auto startupProject = ProjectExplorer::ProjectManager::startupProject();
if (!startupProject)
return;
FilePathModel model(startupProject);
auto exportDir = startupProject->projectFilePath().parentDir();
if (!exportDir.parentDir().isEmpty())
exportDir = exportDir.parentDir();
exportDir = exportDir.pathAppended(startupProject->displayName() + "_export");
AssetExporter assetExporter(m_view, startupProject, m_projectManager.projectStorageDependencies());
AssetExportDialog assetExporterDialog(exportDir, assetExporter, model);
assetExporterDialog.exec();
}
void AssetExporterPlugin::addActions()
{
auto exportAction = new QAction(tr("Export Components"), this);
exportAction->setToolTip(tr("Export components in the current project."));
connect(exportAction, &QAction::triggered, this, &AssetExporterPlugin::onExport);
Core::Command *cmd = Core::ActionManager::registerAction(exportAction, Constants::EXPORT_QML);
// Add action to build menu
Core::ActionContainer *buildMenu =
Core::ActionManager::actionContainer(ProjectExplorer::Constants::M_BUILDPROJECT);
buildMenu->addAction(cmd, ProjectExplorer::Constants::G_BUILD_RUN);
}
void AssetExporterPlugin::updateActions()
{
auto project = ProjectExplorer::ProjectManager::startupProject();
QAction* const exportAction = Core::ActionManager::command(Constants::EXPORT_QML)->action();
exportAction->setEnabled(project && !project->needsConfiguration());
}
QString AssetExporterPlugin::metaInfo() const
{
return QLatin1String(":/assetexporterplugin/assetexporterplugin.metainfo");
}
} //QmlDesigner

View File

@@ -1,35 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#pragma once
#include <iwidgetplugin.h>
namespace QmlDesigner {
class AssetExporter;
class AssetExporterView;
class AssetExporterPlugin : public QObject, QmlDesigner::IWidgetPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QmlDesignerPlugin" FILE "assetexporterplugin.json")
Q_DISABLE_COPY(AssetExporterPlugin)
Q_INTERFACES(QmlDesigner::IWidgetPlugin)
public:
AssetExporterPlugin();
QString metaInfo() const final;
QString pluginName() const final;
private:
void onExport();
void addActions();
void updateActions();
AssetExporterView *m_view = nullptr;
class QmlDesignerProjectManager &m_projectManager;
};
} // namespace QmlDesigner

View File

@@ -1,6 +0,0 @@
{
"Vendor" : "The Qt Company Ltd",
"Category" : "Qt Quick",
"Description" : "Plugin for exporting assets and QML from QmlDesigner",
"Url" : "http://www.qt.io"
}

View File

@@ -1,5 +0,0 @@
<RCC>
<qresource prefix="/assetexporterplugin">
<file>assetexporterplugin.metainfo</file>
</qresource>
</RCC>

View File

@@ -1,142 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "assetexporterview.h"
#include "qmlitemnode.h"
#include "rewriterview.h"
#include "coreplugin/editormanager/editormanager.h"
#include "coreplugin/editormanager/ieditor.h"
#include "coreplugin/modemanager.h"
#include "coreplugin/coreconstants.h"
#include <QLoggingCategory>
namespace {
Q_LOGGING_CATEGORY(loggerInfo, "qtc.designer.assetExportPlugin.view", QtInfoMsg)
Q_LOGGING_CATEGORY(loggerWarn, "qtc.designer.assetExportPlugin.view", QtWarningMsg)
//Q_LOGGING_CATEGORY(loggerError, "qtc.designer.assetExportPlugin.view", QtCriticalMsg)
const int RetryIntervalMs = 500;
const int MinRetry = 2;
}
namespace QmlDesigner {
AssetExporterView::AssetExporterView(ExternalDependenciesInterface &externalDependencies)
: AbstractView(externalDependencies)
, m_timer(this)
{
m_timer.setInterval(RetryIntervalMs);
// We periodically check if file is loaded.
connect(&m_timer, &QTimer::timeout, this, &AssetExporterView::handleTimerTimeout);
}
bool AssetExporterView::loadQmlFile(const Utils::FilePath &path, uint timeoutSecs)
{
qCDebug(loggerInfo) << "Load file" << path;
if (loadingState() == LoadState::Busy)
return false;
setState(LoadState::Busy);
m_retryCount = std::max(MinRetry, static_cast<int>((timeoutSecs * 1000) / RetryIntervalMs));
m_currentEditor = Core::EditorManager::openEditor(path, Utils::Id(),
Core::EditorManager::DoNotMakeVisible);
Core::ModeManager::activateMode(Core::Constants::MODE_DESIGN);
Core::ModeManager::setFocusToCurrentMode();
m_timer.start();
return true;
}
Utils::Result AssetExporterView::saveQmlFile() const
{
if (!m_currentEditor) {
qCDebug(loggerWarn) << "Saving QML file failed. No editor.";
return Utils::Result::Error("Saving QML file failed. No editor.");
}
return m_currentEditor->document()->save();
}
void AssetExporterView::modelAttached(Model *model)
{
if (model->rewriterView() && !model->rewriterView()->errors().isEmpty())
setState(LoadState::QmlErrorState);
AbstractView::modelAttached(model);
}
void AssetExporterView::
instanceInformationsChanged(const QMultiHash<ModelNode, InformationName> &informationChangeHash)
{
if (inErrorState() || loadingState() == LoadState::Loaded)
return; // Already reached error or connected state.
// We expect correct dimensions are available if the rootnode's
// information change message is received.
const auto nodes = informationChangeHash.keys();
bool hasRootNode = std::any_of(nodes.begin(), nodes.end(), [](const ModelNode &n) {
return n.isRootNode();
});
if (hasRootNode)
handleMaybeDone();
}
void AssetExporterView::instancesPreviewImageChanged([[maybe_unused]] const QVector<ModelNode> &nodeList)
{
emit previewChanged();
}
bool AssetExporterView::inErrorState() const
{
return m_state == LoadState::Exausted || m_state == LoadState::QmlErrorState;
}
bool AssetExporterView::isLoaded() const
{
return isAttached() && QmlItemNode(rootModelNode()).isValid();
}
void AssetExporterView::setState(AssetExporterView::LoadState state)
{
if (state != m_state) {
m_state = state;
qCDebug(loggerInfo) << "Loading state changed" << m_state;
if (inErrorState() || m_state == LoadState::Loaded) {
m_timer.stop();
// TODO: Send the loaded signal with a delay. The assumption that model attached and a
// valid root object is enough to declare a QML file is ready is incorrect. A ideal
// solution would be that the QML Puppet notifies file ready signal.
if (m_state == LoadState::Loaded)
QTimer::singleShot(2000, this, &AssetExporterView::loadingFinished);
else
emit loadingError(m_state);
}
}
}
void AssetExporterView::handleMaybeDone()
{
if (isLoaded())
setState(LoadState::Loaded);
}
void AssetExporterView::handleTimerTimeout()
{
if (!inErrorState() && loadingState() != LoadState::Loaded)
handleMaybeDone();
if (--m_retryCount < 0)
setState(LoadState::Exausted);
}
}
QT_BEGIN_NAMESPACE
QDebug operator<<(QDebug os, const QmlDesigner::AssetExporterView::LoadState &s)
{
os << static_cast<std::underlying_type<QmlDesigner::AssetExporterView::LoadState>::type>(s);
return os;
}
QT_END_NAMESPACE

View File

@@ -1,62 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#pragma once
#include "abstractview.h"
#include <utils/filepath.h>
#include <QObject>
#include <QTimer>
namespace Core { class IEditor; }
namespace QmlDesigner {
class AssetExporterView : public AbstractView
{
Q_OBJECT
public:
enum class LoadState {
Idle = 1,
Busy,
Exausted,
QmlErrorState,
Loaded
};
AssetExporterView(ExternalDependenciesInterface &externalDependencies);
bool loadQmlFile(const Utils::FilePath &path, uint timeoutSecs = 10);
Utils::Result saveQmlFile() const;
void modelAttached(Model *model) override;
void instanceInformationsChanged(const QMultiHash<ModelNode, InformationName> &informationChangeHash) override;
void instancesPreviewImageChanged(const QVector<ModelNode> &nodeList) override;
LoadState loadingState() const { return m_state; }
bool inErrorState() const;
signals:
void loadingFinished();
void loadingError(LoadState);
void previewChanged();
private:
bool isLoaded() const;
void setState(LoadState state);
void handleMaybeDone();
void handleTimerTimeout();
Core::IEditor *m_currentEditor = nullptr;
QTimer m_timer;
int m_retryCount = 0;
LoadState m_state = LoadState::Idle;
bool m_waitForPuppet = false;
};
}
QT_BEGIN_NAMESPACE
QDebug operator<<(QDebug os, const QmlDesigner::AssetExporterView::LoadState &s);
QT_END_NAMESPACE

View File

@@ -1,66 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#pragma once
#include <auxiliarydata.h>
namespace QmlDesigner {
namespace Constants {
const char EXPORT_QML[] = "Designer.ExportPlugin.ExportQml";
const char TASK_CATEGORY_ASSET_EXPORT[] = "AssetExporter.Export";
//***************************************************************************
// Metadata tags
//***************************************************************************
// Plugin info tags
const char PluginInfoTag[] = "pluginInfo";
const char MetadataVersionTag[] = "metadataVersion";
const char DocumentInfoTag[] = "documentInfo";
const char DocumentNameTag[] = "name";
// Layer data tags
const char ArtboardListTag[] = "artboards";
const char NameTag[] = "name";
const char XPosTag[] = "x";
const char YPosTag[] = "y";
const char WidthTag[] = "width";
const char HeightTag[] = "height";
const char MetadataTag[] = "metadata";
const char ChildrenTag[] = "children";
const char CustomIdTag[] = "customId";
const char QmlIdTag[] = "qmlId";
const char ExportTypeTag[] = "exportType";
const char ExportTypeComponent[] = "component";
const char ExportTypeChild[] = "child";
const char QmlPropertiesTag[] = "qmlProperties";
const char ImportsTag[] = "extraImports";
const char UuidTag[] = "uuid";
const char ClipTag[] = "clip";
const char AssetDataTag[] = "assetData";
const char ReferenceAssetTag[] = "referenceAsset";
const char AssetPathTag[] = "assetPath";
const char AssetBoundsTag[] = "assetBounds";
const char OpacityTag[] = "opacity";
const char TypeNameTag[] = "typeName";
const char TypeIdTag[] = "typeId";
const char TextDetailsTag[] = "textDetails";
const char FontFamilyTag[] = "fontFamily";
const char FontSizeTag[] = "fontSize";
const char FontStyleTag[] = "fontStyle";
const char LetterSpacingTag[] = "kerning";
const char TextColorTag[] = "textColor";
const char TextContentTag[] = "contents";
const char IsMultilineTag[] = "multiline";
const char LineHeightTag[] = "lineHeight";
const char HAlignTag[] = "horizontalAlignment";
const char VAlignTag[] = "verticalAlignment";
}
}

View File

@@ -1,158 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "componentexporter.h"
#include "assetexporter.h"
#include "assetexportpluginconstants.h"
#include "exportnotification.h"
#include "dumpers/nodedumper.h"
#include "model.h"
#include "nodeabstractproperty.h"
#include "nodemetainfo.h"
#include "qmlitemnode.h"
#include "rewriterview.h"
#include "utils/qtcassert.h"
#include <QJsonArray>
#include <QJsonObject>
#include <QLoggingCategory>
#include <QPainter>
namespace {
Q_LOGGING_CATEGORY(loggerInfo, "qtc.designer.assetExportPlugin.modelExporter", QtInfoMsg)
}
namespace QmlDesigner {
using namespace Constants;
std::vector<std::unique_ptr<Internal::NodeDumperCreatorBase>> Component::m_readers;
Component::Component(AssetExporter &exporter, const ModelNode &rootNode):
m_exporter(exporter),
m_rootNode(rootNode)
{
m_name = m_rootNode.id();
if (m_name.isEmpty())
m_name = QString::fromUtf8(m_rootNode.type());
}
QJsonObject Component::json() const
{
return m_json;
}
AssetExporter &Component::exporter()
{
return m_exporter;
}
void Component::exportComponent()
{
QTC_ASSERT(m_rootNode.isValid(), return);
m_json = nodeToJson(m_rootNode);
// Change the export type to component
QJsonObject metadata = m_json.value(MetadataTag).toObject();
metadata.insert(ExportTypeTag, ExportTypeComponent);
addReferenceAsset(metadata);
m_json.insert(MetadataTag, metadata);
addImports();
}
const QString &Component::name() const
{
return m_name;
}
NodeDumper *Component::createNodeDumper(const ModelNode &node) const
{
std::unique_ptr<NodeDumper> reader;
for (auto &dumperCreator: m_readers) {
std::unique_ptr<NodeDumper> r(dumperCreator->instance(node));
if (r->isExportable()) {
if (reader) {
if (reader->priority() < r->priority())
reader = std::move(r);
} else {
reader = std::move(r);
}
}
}
if (!reader)
qCDebug(loggerInfo()) << "No dumper for node" << node;
return reader.release();
}
QJsonObject Component::nodeToJson(const ModelNode &node)
{
QJsonObject jsonObject;
// Don't export States, Connection, Timeline etc nodes.
if (!node.metaInfo().isQtQuickItem())
return {};
std::unique_ptr<NodeDumper> dumper(createNodeDumper(node));
if (dumper) {
jsonObject = dumper->json(*this);
} else {
ExportNotification::addError(tr("Error exporting node %1. Cannot parse type %2.")
.arg(node.id()).arg(QString::fromUtf8(node.type())));
}
QJsonArray children;
for (const ModelNode &childnode : node.directSubModelNodes()) {
const QJsonObject childJson = nodeToJson(childnode);
if (!childJson.isEmpty())
children.append(childJson);
}
if (!children.isEmpty())
jsonObject.insert(ChildrenTag, children);
return jsonObject;
}
void Component::addReferenceAsset(QJsonObject &metadataObject) const
{
QPixmap refAsset = m_exporter.generateAsset(m_rootNode);
stichChildrendAssets(m_rootNode, refAsset);
Utils::FilePath refAssetPath = m_exporter.assetPath(m_rootNode, this, "_ref");
m_exporter.exportAsset(refAsset, refAssetPath);
QJsonObject assetData;
if (metadataObject.contains(AssetDataTag))
assetData = metadataObject[AssetDataTag].toObject();
assetData.insert(ReferenceAssetTag, refAssetPath.toUrlishString());
metadataObject.insert(AssetDataTag, assetData);
}
void Component::stichChildrendAssets(const ModelNode &node, QPixmap &parentPixmap) const
{
if (!node.hasAnySubModelNodes())
return;
QPainter painter(&parentPixmap);
for (const ModelNode &child : node.directSubModelNodes()) {
QPixmap childPixmap = m_exporter.generateAsset(child);
if (childPixmap.isNull())
continue;
stichChildrendAssets(child, childPixmap);
QTransform cTransform = QmlObjectNode(child).toQmlItemNode().instanceTransform();
painter.setTransform(cTransform);
painter.drawPixmap(QPoint(0, 0), childPixmap);
}
painter.end();
}
void Component::addImports()
{
QJsonArray importsArray;
for (const Import &import : m_rootNode.model()->imports())
importsArray.append(import.toString());
if (!importsArray.empty())
m_json.insert(Constants::ImportsTag, importsArray);
}
} // namespace QmlDesigner

View File

@@ -1,78 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#pragma once
#include <QByteArrayList>
#include <QCoreApplication>
#include <QJsonObject>
#include <memory>
#include <vector>
#include "utils/qtcassert.h"
QT_BEGIN_NAMESPACE
class QJsonArray;
QT_END_NAMESPACE
namespace QmlDesigner {
class AssetExporter;
class ModelNode;
class Component;
class NodeDumper;
namespace Internal {
class NodeDumperCreatorBase
{
public:
virtual ~NodeDumperCreatorBase() {}
protected:
virtual NodeDumper *instance(const ModelNode &) const = 0;
friend Component;
};
template<class T>
class NodeDumperCreator : public NodeDumperCreatorBase
{
public:
NodeDumperCreator() = default;
~NodeDumperCreator() = default;
protected:
NodeDumper *instance(const ModelNode &node) const { return new T(node); }
};
} //Internal
class Component
{
Q_DECLARE_TR_FUNCTIONS(Component);
public:
Component(AssetExporter& exporter, const ModelNode &rootNode);
void exportComponent();
const QString &name() const;
QJsonObject json() const;
AssetExporter &exporter();
template<typename T> static void addNodeDumper()
{
QTC_ASSERT((std::is_base_of<NodeDumper, T>::value), return);
m_readers.push_back(std::make_unique<Internal::NodeDumperCreator<T>>());
}
private:
NodeDumper* createNodeDumper(const ModelNode &node) const;
QJsonObject nodeToJson(const ModelNode &node);
void addReferenceAsset(QJsonObject &metadataObject) const;
void stichChildrendAssets(const ModelNode &node, QPixmap &parentPixmap) const;
void addImports();
private:
AssetExporter& m_exporter;
const ModelNode &m_rootNode;
QString m_name;
QJsonObject m_json;
static std::vector<std::unique_ptr<Internal::NodeDumperCreatorBase>> m_readers;
};
}

View File

@@ -1,47 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "assetnodedumper.h"
#include "assetexportpluginconstants.h"
#include "assetexporter.h"
#include "qmlitemnode.h"
#include "componentexporter.h"
#include "utils/fileutils.h"
#include <QPixmap>
namespace QmlDesigner {
using namespace Constants;
AssetNodeDumper::AssetNodeDumper(const ModelNode &node)
: ItemNodeDumper(node)
{
}
bool AssetNodeDumper::isExportable() const
{
auto qtQuickImageMetaInfo = model()->qtQuickImageMetaInfo();
auto qtQuickRectangleMetaInfo = model()->qtQuickRectangleMetaInfo();
return metaInfo().isBasedOn(qtQuickImageMetaInfo, qtQuickRectangleMetaInfo);
}
QJsonObject AssetNodeDumper::json(Component &component) const
{
QJsonObject jsonObject = ItemNodeDumper::json(component);
AssetExporter &exporter = component.exporter();
const Utils::FilePath assetPath = exporter.assetPath(m_node, &component);
exporter.exportAsset(exporter.generateAsset(m_node), assetPath);
QJsonObject assetData;
assetData.insert(AssetPathTag, assetPath.toUrlishString());
QJsonObject metadata = jsonObject.value(MetadataTag).toObject();
metadata.insert(AssetDataTag, assetData);
jsonObject.insert(MetadataTag, metadata);
return jsonObject;
}
}

View File

@@ -1,20 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#pragma once
#include "itemnodedumper.h"
namespace QmlDesigner {
class Component;
class AssetNodeDumper : public ItemNodeDumper
{
public:
AssetNodeDumper(const ModelNode &node);
~AssetNodeDumper() override = default;
bool isExportable() const override;
int priority() const override { return 200; }
QJsonObject json(Component &component) const override;
};
}

View File

@@ -1,76 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "itemnodedumper.h"
#include "assetexportpluginconstants.h"
#include "assetexporter.h"
#include "componentexporter.h"
#include "qmlitemnode.h"
#include "annotation.h"
namespace {
static QString capitalize(const QString &str)
{
if (str.isEmpty())
return {};
QString tmp = str;
tmp[0] = QChar(str[0]).toUpper().toLatin1();
return tmp;
}
}
namespace QmlDesigner {
using namespace Constants;
ItemNodeDumper::ItemNodeDumper(const ModelNode &node)
: NodeDumper(node)
{
}
bool QmlDesigner::ItemNodeDumper::isExportable() const
{
return metaInfo().isQtQuickItem();
}
QJsonObject QmlDesigner::ItemNodeDumper::json([[maybe_unused]] QmlDesigner::Component &component) const
{
const QmlObjectNode &qmlObjectNode = objectNode();
QJsonObject jsonObject;
const QString qmlId = qmlObjectNode.id();
QString name = m_node.simplifiedTypeName();
if (!qmlId.isEmpty())
name.append("_" + capitalize(qmlId));
jsonObject.insert(NameTag, name);
// Position relative to parent
QmlItemNode itemNode = qmlObjectNode.toQmlItemNode();
QPointF pos = itemNode.instancePosition();
jsonObject.insert(XPosTag, pos.x());
jsonObject.insert(YPosTag, pos.y());
// size
QSizeF size = itemNode.instanceSize();
jsonObject.insert(WidthTag, size.width());
jsonObject.insert(HeightTag, size.height());
QJsonObject metadata;
metadata.insert(QmlIdTag, qmlId);
metadata.insert(UuidTag, uuid());
metadata.insert(ExportTypeTag, ExportTypeChild);
metadata.insert(TypeNameTag, QString::fromLatin1(m_node.type()));
if (m_node.hasCustomId())
metadata.insert(CustomIdTag, m_node.customId());
QString typeId = component.exporter().componentUuid(m_node);
if (!typeId.isEmpty())
metadata.insert(TypeIdTag, typeId);
jsonObject.insert(MetadataTag, metadata);
return jsonObject;
}
}

View File

@@ -1,22 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#pragma once
#include "nodedumper.h"
namespace QmlDesigner {
class ModelNode;
class Component;
class ItemNodeDumper : public NodeDumper
{
public:
ItemNodeDumper(const ModelNode &node);
~ItemNodeDumper() override = default;
int priority() const override { return 100; }
bool isExportable() const override;
QJsonObject json(Component &component) const override;
};
}

View File

@@ -1,28 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "nodedumper.h"
#include "assetexportpluginconstants.h"
#include <auxiliarydataproperties.h>
namespace QmlDesigner {
NodeDumper::NodeDumper(const ModelNode &node)
: m_node(node)
, m_objectNode(node)
, m_metaInfo(node.metaInfo())
, m_model{node.model()}
{
}
QVariant NodeDumper::propertyValue(const PropertyName &name) const
{
return m_objectNode.instanceValue(name);
}
QString NodeDumper::uuid() const
{
return m_node.auxiliaryDataWithDefault(uuidProperty).toString();
}
}

View File

@@ -1,40 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#pragma once
#include "qmlobjectnode.h"
#include <QJsonObject>
#include <QByteArrayList>
namespace QmlDesigner {
class Component;
class ModelNode;
class NodeDumper
{
public:
NodeDumper(const ModelNode &node);
virtual ~NodeDumper() = default;
virtual int priority() const = 0;
virtual bool isExportable() const = 0;
virtual QJsonObject json(Component& component) const = 0;
const NodeMetaInfo &metaInfo() const { return m_metaInfo; }
const QmlObjectNode& objectNode() const { return m_objectNode; }
QVariant propertyValue(const PropertyName &name) const;
QString uuid() const;
Model *model() const { return m_model; }
protected:
const ModelNode &m_node;
private:
QmlObjectNode m_objectNode;
NodeMetaInfo m_metaInfo;
Model *m_model = nullptr;
};
}

View File

@@ -1,90 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "textnodedumper.h"
#include "assetexportpluginconstants.h"
#include <model.h>
#include <QColor>
#include <QFontInfo>
#include <QFontMetricsF>
#include <QHash>
#include <QtMath>
#include <private/qquicktext_p.h>
#include <algorithm>
namespace {
const QHash<QString, QString> AlignMapping{
{"AlignRight", "RIGHT"},
{"AlignHCenter", "CENTER"},
{"AlignJustify", "JUSTIFIED"},
{"AlignLeft", "LEFT"},
{"AlignTop", "TOP"},
{"AlignVCenter", "CENTER"},
{"AlignBottom", "BOTTOM"}
};
QString toJsonAlignEnum(QString value) {
if (value.isEmpty() || !AlignMapping.contains(value))
return {};
return AlignMapping[value];
}
}
namespace QmlDesigner {
using namespace Constants;
TextNodeDumper::TextNodeDumper(const ModelNode &node)
: ItemNodeDumper(node)
{
}
bool TextNodeDumper::isExportable() const
{
auto qtQuickTextMetaInfo = model()->qtQuickTextMetaInfo();
auto qtQuickControlsLabelMetaInfo = model()->qtQuickControlsLabelMetaInfo();
return metaInfo().isBasedOn(qtQuickTextMetaInfo, qtQuickControlsLabelMetaInfo);
}
QJsonObject TextNodeDumper::json([[maybe_unused]] Component &component) const
{
QJsonObject jsonObject = ItemNodeDumper::json(component);
QJsonObject textDetails;
textDetails.insert(TextContentTag, propertyValue("text").toString());
QFont font = propertyValue("font").value<QFont>();
QFontInfo fontInfo(font);
textDetails.insert(FontFamilyTag, fontInfo.family());
textDetails.insert(FontStyleTag, fontInfo.styleName());
textDetails.insert(FontSizeTag, fontInfo.pixelSize());
textDetails.insert(LetterSpacingTag, font.letterSpacing());
QColor fontColor(propertyValue("font.color").toString());
textDetails.insert(TextColorTag, fontColor.name(QColor::HexArgb));
textDetails.insert(HAlignTag, toJsonAlignEnum(propertyValue("horizontalAlignment").toString()));
textDetails.insert(VAlignTag, toJsonAlignEnum(propertyValue("verticalAlignment").toString()));
textDetails.insert(IsMultilineTag, propertyValue("wrapMode").toString().compare("NoWrap") != 0);
// Calculate line height in pixels
QFontMetricsF fm(font);
auto lineHeightMode = propertyValue("lineHeightMode").value<QQuickText::LineHeightMode>();
double lineHeight = propertyValue("lineHeight").toDouble();
qreal lineHeightPx = (lineHeightMode == QQuickText::FixedHeight) ?
lineHeight : qCeil(fm.height()) * lineHeight;
textDetails.insert(LineHeightTag, lineHeightPx);
QJsonObject metadata = jsonObject.value(MetadataTag).toObject();
metadata.insert(TextDetailsTag, textDetails);
jsonObject.insert(MetadataTag, metadata);
return jsonObject;
}
}

View File

@@ -1,21 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#pragma once
#include "itemnodedumper.h"
namespace QmlDesigner {
class Component;
class TextNodeDumper : public ItemNodeDumper
{
public:
TextNodeDumper(const ModelNode &node);
~TextNodeDumper() override = default;
bool isExportable() const override;
int priority() const override { return 200; }
QJsonObject json(Component &component) const override;
};
}

View File

@@ -1,41 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "exportnotification.h"
#include "assetexportpluginconstants.h"
#include "projectexplorer/taskhub.h"
#include <QLoggingCategory>
namespace {
Q_LOGGING_CATEGORY(loggerDebug, "qtc.designer.assetExportPlugin.exportNotification", QtDebugMsg)
}
using namespace ProjectExplorer;
namespace {
static void addTask(Task::TaskType type, const QString &desc)
{
qCDebug(loggerDebug) << desc;
Task task(type, desc, {}, -1, QmlDesigner::Constants::TASK_CATEGORY_ASSET_EXPORT);
TaskHub::addTask(task);
}
}
namespace QmlDesigner {
void ExportNotification::addError(const QString &errMsg)
{
addTask(Task::Error, errMsg);
}
void ExportNotification::addWarning(const QString &warningMsg)
{
addTask(Task::Warning, warningMsg);
}
void ExportNotification::addInfo(const QString &infoMsg)
{
addTask(Task::Unknown, infoMsg);
}
} // QmlDesigner

View File

@@ -1,16 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#pragma once
#include <QString>
namespace QmlDesigner {
class ExportNotification
{
public:
static void addError(const QString &errMsg);
static void addWarning(const QString &warningMsg);
static void addInfo(const QString &infoMsg);
};
} // QmlDesigner

View File

@@ -1,143 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "filepathmodel.h"
#include "exportnotification.h"
#include <projectexplorer/project.h>
#include <projectexplorer/projectnodes.h>
#include <utils/async.h>
#include <QLoggingCategory>
#include <QTimer>
using namespace ProjectExplorer;
namespace {
Q_LOGGING_CATEGORY(loggerError, "qtc.designer.assetExportPlugin.filePathModel", QtCriticalMsg)
Q_LOGGING_CATEGORY(loggerInfo, "qtc.designer.assetExportPlugin.filePathModel", QtInfoMsg)
void findQmlFiles(QPromise<Utils::FilePath> &promise, const Project *project)
{
if (!project || promise.isCanceled())
return;
int index = 0;
project->files([&promise, &index](const Node* node) ->bool {
if (promise.isCanceled())
return false;
Utils::FilePath path = node->filePath();
bool isComponent = !path.fileName().isEmpty() && path.fileName().front().isUpper();
if (isComponent && node->filePath().endsWith(".ui.qml"))
promise.addResult(path, index++);
return true;
});
}
}
namespace QmlDesigner {
FilePathModel::FilePathModel(ProjectExplorer::Project *project, QObject *parent)
: QAbstractListModel(parent),
m_project(project)
{
QTimer::singleShot(0, this, &FilePathModel::processProject);
}
FilePathModel::~FilePathModel()
{
if (m_preprocessWatcher && !m_preprocessWatcher->isCanceled() &&
!m_preprocessWatcher->isFinished()) {
ExportNotification::addInfo(tr("Canceling file preparation."));
m_preprocessWatcher->cancel();
m_preprocessWatcher->waitForFinished();
qCDebug(loggerInfo) << "Canceled file preparation.";
}
}
Qt::ItemFlags FilePathModel::flags(const QModelIndex &index) const
{
Qt::ItemFlags itemFlags = QAbstractListModel::flags(index);
if (index.isValid())
itemFlags |= Qt::ItemIsUserCheckable;
return itemFlags;
}
int FilePathModel::rowCount(const QModelIndex &parent) const
{
if (!parent.isValid())
return m_files.size();
return 0;
}
QVariant FilePathModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return {};
switch (role) {
case Qt::DisplayRole:
return m_files[index.row()].toUserOutput();
case Qt::CheckStateRole:
return m_skipped.count(m_files[index.row()]) ? Qt::Unchecked : Qt::Checked;
default:
break;
}
return {};
}
bool FilePathModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (!index.isValid() || role != Qt::CheckStateRole)
return false;
const Utils::FilePath path = m_files[index.row()];
if (value == Qt::Checked)
m_skipped.erase(path);
else
m_skipped.insert(path);
emit dataChanged(index, index);
return true;
}
Utils::FilePaths FilePathModel::files() const
{
Utils::FilePaths selectedPaths;
std::copy_if(m_files.begin(), m_files.end(), std::back_inserter(selectedPaths),
[this](const Utils::FilePath &path) {
return !m_skipped.count(path);
});
return selectedPaths;
}
void FilePathModel::processProject()
{
if (m_preprocessWatcher && !m_preprocessWatcher->isCanceled() &&
!m_preprocessWatcher->isFinished()) {
qCDebug(loggerError) << "Previous model load not finished.";
return;
}
beginResetModel();
m_preprocessWatcher.reset(new QFutureWatcher<Utils::FilePath>(this));
connect(m_preprocessWatcher.get(),
&QFutureWatcher<Utils::FilePath>::resultReadyAt,
this,
[this](int resultIndex) {
beginInsertRows(index(0, 0), m_files.size(), m_files.size());
m_files.append(m_preprocessWatcher->resultAt(resultIndex));
endInsertRows();
});
connect(m_preprocessWatcher.get(), &QFutureWatcher<Utils::FilePath>::finished,
this, &FilePathModel::endResetModel);
QFuture<Utils::FilePath> f = Utils::asyncRun(&findQmlFiles, m_project);
m_preprocessWatcher->setFuture(f);
}
}

View File

@@ -1,43 +0,0 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#pragma once
#include <utils/filepath.h>
#include <QAbstractListModel>
#include <QCoreApplication>
#include <QFutureWatcher>
#include <memory>
#include <unordered_set>
namespace ProjectExplorer {
class Project;
}
namespace QmlDesigner {
class FilePathModel : public QAbstractListModel
{
Q_DECLARE_TR_FUNCTIONS(QmlDesigner::FilePathModel)
public:
FilePathModel(ProjectExplorer::Project *project, QObject *parent = nullptr);
~FilePathModel() override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
Utils::FilePaths files() const;
private:
void processProject();
ProjectExplorer::Project *m_project = nullptr;
std::unique_ptr<QFutureWatcher<Utils::FilePath>> m_preprocessWatcher;
std::unordered_set<Utils::FilePath> m_skipped;
Utils::FilePaths m_files;
};
}

Some files were not shown because too many files have changed in this diff Show More