From 34f51df1ea2df8beb4091b48c0015771f02cf923 Mon Sep 17 00:00:00 2001 From: Ali Kianian Date: Thu, 5 Sep 2024 17:46:26 +0300 Subject: [PATCH] QmlDesigner: Add code editor for Effect Composer Task-number: QDS-13443 Change-Id: I02c7a85336f283e0e55bab24459a91fa299abb40 Reviewed-by: Mahmoud Badri Reviewed-by: Miikka Heikkinen --- .../qtcreator/qmldesigner/designericons.json | 6 + .../EffectComposer.qml | 6 + .../EffectComposerTopBar.qml | 15 +- .../EffectCompositionNode.qml | 24 ++ .../imports/HelperWidgets/Section.qml | 9 + src/plugins/effectcomposer/CMakeLists.txt | 2 + .../effectcomposer/compositionnode.cpp | 67 +++++- src/plugins/effectcomposer/compositionnode.h | 21 ++ .../effectcomposer/effectcodeeditorwidget.cpp | 142 +++++++++++ .../effectcomposer/effectcodeeditorwidget.h | 63 +++++ .../effectcomposer/effectcomposermodel.cpp | 82 ++++++- .../effectcomposer/effectcomposermodel.h | 8 + .../effectshaderscodeeditor.cpp | 226 ++++++++++++++++++ .../effectcomposer/effectshaderscodeeditor.h | 61 +++++ .../bindingeditor/bindingeditorwidget.cpp | 1 - .../components/componentcore/designericons.h | 2 + .../componentcore/modelnodeoperations.cpp | 11 +- 17 files changed, 725 insertions(+), 21 deletions(-) create mode 100644 src/plugins/effectcomposer/effectcodeeditorwidget.cpp create mode 100644 src/plugins/effectcomposer/effectcodeeditorwidget.h create mode 100644 src/plugins/effectcomposer/effectshaderscodeeditor.cpp create mode 100644 src/plugins/effectcomposer/effectshaderscodeeditor.h diff --git a/share/qtcreator/qmldesigner/designericons.json b/share/qtcreator/qmldesigner/designericons.json index 005494b4715..f4f60b7412c 100644 --- a/share/qtcreator/qmldesigner/designericons.json +++ b/share/qtcreator/qmldesigner/designericons.json @@ -241,6 +241,9 @@ "LocalOrientIcon": { "iconName": "localOrient_medium" }, + "LiveUpdateIcon": { + "iconName": "restartParticles_medium" + }, "MoveToolIcon": { "iconName": "move_medium" }, @@ -276,6 +279,9 @@ "SplitViewIcon": { "iconName": "splitScreen_medium" }, + "SyncIcon": { + "iconName": "updateContent_medium" + }, "ToggleGroupIcon": { "Off": { "iconName": "selectOutline_medium" diff --git a/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectComposer.qml b/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectComposer.qml index 16525678be8..81c01d5f54e 100644 --- a/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectComposer.qml +++ b/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectComposer.qml @@ -195,6 +195,10 @@ Item { onAssignToSelectedClicked: { root.backendModel.assignToSelected() } + + onOpenShadersCodeEditor: { + root.backendModel.openMainShadersCodeEditor() + } } SplitView { @@ -366,6 +370,8 @@ Item { expanded = wasExpanded dragAnimation.enabled = true } + + onOpenShadersCodeEditor: (idx) => root.backendModel.openShadersCodeEditor(idx) } } // Repeater } // Column diff --git a/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectComposerTopBar.qml b/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectComposerTopBar.qml index 799dbeeddc3..c6e8abe11a3 100644 --- a/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectComposerTopBar.qml +++ b/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectComposerTopBar.qml @@ -19,6 +19,7 @@ Rectangle { signal saveClicked signal saveAsClicked signal assignToSelectedClicked + signal openShadersCodeEditor Row { spacing: 5 @@ -48,12 +49,24 @@ Rectangle { style: StudioTheme.Values.viewBarButtonStyle buttonIcon: StudioTheme.Constants.saveAs_medium tooltip: qsTr("Save current composition with a new name") - enabled: root.backendModel ? root.backendModel.isEnabled && root.backendModel.currentComposition !== "" + enabled: root.backendModel ? root.backendModel.isEnabled + && root.backendModel.currentComposition !== "" : false onClicked: root.saveAsClicked() } + HelperWidgets.AbstractButton { + style: StudioTheme.Values.viewBarButtonStyle + buttonIcon: StudioTheme.Constants.codeEditor_medium + tooltip: qsTr("Open Code") + enabled: root.backendModel ? root.backendModel.isEnabled + && root.backendModel.currentComposition !== "" + : false + + onClicked: root.openShadersCodeEditor() + } + HelperWidgets.AbstractButton { style: StudioTheme.Values.viewBarButtonStyle buttonIcon: StudioTheme.Constants.assignTo_medium diff --git a/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectCompositionNode.qml b/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectCompositionNode.qml index a606461b5c5..ef59468ed60 100644 --- a/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectCompositionNode.qml +++ b/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectCompositionNode.qml @@ -30,10 +30,34 @@ HelperWidgets.Section { eyeEnabled: nodeEnabled eyeButtonToolTip: qsTr("Enable/Disable Node") + signal openShadersCodeEditor(index: int) + onEyeButtonClicked: { nodeEnabled = root.eyeEnabled } + icons: HelperWidgets.IconButton { + icon: StudioTheme.Constants.codeEditor_medium + transparentBg: true + buttonSize: 21 + iconSize: StudioTheme.Values.smallIconFontSize + iconColor: StudioTheme.Values.themeTextColor + iconScale: containsMouse ? 1.2 : 1 + implicitWidth: width + onClicked: root.openShadersCodeEditor(index) + } + + content: Label { + text: root.caption + color: root.labelColor + elide: Text.ElideRight + font.pixelSize: root.sectionFontSize + font.capitalization: root.labelCapitalization + anchors.verticalCenter: parent?.verticalCenter + textFormat: Text.RichText + leftPadding: StudioTheme.Values.toolbarSpacing + } + Column { spacing: 10 diff --git a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/Section.qml b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/Section.qml index 245b8506a28..232574a2207 100644 --- a/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/Section.qml +++ b/share/qtcreator/qmldesigner/propertyEditorQmlSources/imports/HelperWidgets/Section.qml @@ -39,6 +39,8 @@ Item { textFormat: Text.RichText } + property Item icons + property int leftPadding: StudioTheme.Values.sectionLeftPadding property int rightPadding: 0 property int topPadding: StudioTheme.Values.sectionHeadSpacerHeight @@ -214,6 +216,13 @@ Item { } } + Item { + id: iconsContent + height: header.height + children: [ section.icons ] + Layout.preferredWidth: childrenRect.width + } + IconButton { id: arrow icon: StudioTheme.Constants.sectionToggle diff --git a/src/plugins/effectcomposer/CMakeLists.txt b/src/plugins/effectcomposer/CMakeLists.txt index 71805ea1be2..4e3e7283b3b 100644 --- a/src/plugins/effectcomposer/CMakeLists.txt +++ b/src/plugins/effectcomposer/CMakeLists.txt @@ -6,12 +6,14 @@ add_qtc_plugin(EffectComposer Qt::Core Qt::CorePrivate Qt::Widgets Qt::Qml Qt::QmlPrivate Qt::Quick QtCreator::Utils SOURCES + effectcodeeditorwidget.cpp effectcodeeditorwidget.h effectcomposerplugin.cpp effectcomposerwidget.cpp effectcomposerwidget.h effectcomposerview.cpp effectcomposerview.h effectcomposermodel.cpp effectcomposermodel.h effectcomposernodesmodel.cpp effectcomposernodesmodel.h effectcomposeruniformsmodel.cpp effectcomposeruniformsmodel.h + effectshaderscodeeditor.cpp effectshaderscodeeditor.h effectnode.cpp effectnode.h effectnodescategory.cpp effectnodescategory.h compositionnode.cpp compositionnode.h diff --git a/src/plugins/effectcomposer/compositionnode.cpp b/src/plugins/effectcomposer/compositionnode.cpp index a69cd00e075..5426d55f2cb 100644 --- a/src/plugins/effectcomposer/compositionnode.cpp +++ b/src/plugins/effectcomposer/compositionnode.cpp @@ -3,8 +3,9 @@ #include "compositionnode.h" -#include "effectutils.h" #include "effectcomposeruniformsmodel.h" +#include "effectshaderscodeeditor.h" +#include "effectutils.h" #include "propertyhandler.h" #include "uniform.h" @@ -44,6 +45,8 @@ CompositionNode::CompositionNode(const QString &effectName, const QString &qenPa } } +CompositionNode::~CompositionNode() = default; + QString CompositionNode::fragmentCode() const { return m_fragmentCode; @@ -110,8 +113,8 @@ void CompositionNode::parse(const QString &effectName, const QString &qenPath, c m_name = json.value("name").toString(); m_description = json.value("description").toString(); - m_fragmentCode = EffectUtils::codeFromJsonArray(json.value("fragmentCode").toArray()); - m_vertexCode = EffectUtils::codeFromJsonArray(json.value("vertexCode").toArray()); + setFragmentCode(EffectUtils::codeFromJsonArray(json.value("fragmentCode").toArray())); + setVertexCode(EffectUtils::codeFromJsonArray(json.value("vertexCode").toArray())); if (json.contains("extraMargin")) m_extraMargin = json.value("extraMargin").toInt(); @@ -154,6 +157,36 @@ void CompositionNode::parse(const QString &effectName, const QString &qenPath, c } } +void CompositionNode::ensureShadersCodeEditor() +{ + if (m_shadersCodeEditor) + return; + + m_shadersCodeEditor = Utils::makeUniqueObjectLatePtr(name()); + m_shadersCodeEditor->setFragmentValue(fragmentCode()); + m_shadersCodeEditor->setVertexValue(vertexCode()); + + connect(m_shadersCodeEditor.get(), &EffectShadersCodeEditor::vertexValueChanged, this, [this] { + setVertexCode(m_shadersCodeEditor->vertexValue()); + }); + + connect(m_shadersCodeEditor.get(), &EffectShadersCodeEditor::fragmentValueChanged, this, [this] { + setFragmentCode(m_shadersCodeEditor->fragmentValue()); + }); + + connect( + m_shadersCodeEditor.get(), + &EffectShadersCodeEditor::rebakeRequested, + this, + &CompositionNode::rebakeRequested); +} + +void CompositionNode::requestRebakeIfLiveUpdateMode() +{ + if (m_shadersCodeEditor && m_shadersCodeEditor->liveUpdate()) + emit rebakeRequested(); +} + QList CompositionNode::uniforms() const { return m_uniforms; @@ -189,6 +222,34 @@ void CompositionNode::setRefCount(int count) emit isDepencyChanged(); } +void CompositionNode::setFragmentCode(const QString &fragmentCode) +{ + if (m_fragmentCode == fragmentCode) + return; + + m_fragmentCode = fragmentCode; + emit fragmentCodeChanged(); + + requestRebakeIfLiveUpdateMode(); +} + +void CompositionNode::setVertexCode(const QString &vertexCode) +{ + if (m_vertexCode == vertexCode) + return; + + m_vertexCode = vertexCode; + emit vertexCodeChanged(); + + requestRebakeIfLiveUpdateMode(); +} + +void CompositionNode::openShadersCodeEditor() +{ + ensureShadersCodeEditor(); + m_shadersCodeEditor->showWidget(); +} + QString CompositionNode::name() const { return m_name; diff --git a/src/plugins/effectcomposer/compositionnode.h b/src/plugins/effectcomposer/compositionnode.h index 433468688a2..dcd66072afa 100644 --- a/src/plugins/effectcomposer/compositionnode.h +++ b/src/plugins/effectcomposer/compositionnode.h @@ -5,11 +5,15 @@ #include "effectcomposeruniformsmodel.h" +#include + #include #include namespace EffectComposer { +class EffectShadersCodeEditor; + class CompositionNode : public QObject { Q_OBJECT @@ -18,6 +22,12 @@ class CompositionNode : public QObject Q_PROPERTY(bool nodeEnabled READ isEnabled WRITE setIsEnabled NOTIFY isEnabledChanged) Q_PROPERTY(bool isDependency READ isDependency NOTIFY isDepencyChanged) Q_PROPERTY(QObject *nodeUniformsModel READ uniformsModel NOTIFY uniformsModelChanged) + Q_PROPERTY( + QString fragmentCode + READ fragmentCode + WRITE setFragmentCode + NOTIFY fragmentCodeChanged) + Q_PROPERTY(QString vertexCode READ vertexCode WRITE setVertexCode NOTIFY vertexCodeChanged) public: enum NodeType { @@ -27,6 +37,7 @@ public: }; CompositionNode(const QString &effectName, const QString &qenPath, const QJsonObject &json = {}); + virtual ~CompositionNode(); QString fragmentCode() const; QString vertexCode() const; @@ -54,14 +65,23 @@ public: int extraMargin() const { return m_extraMargin; } + void setFragmentCode(const QString &fragmentCode); + void setVertexCode(const QString &vertexCode); + + void openShadersCodeEditor(); + signals: void uniformsModelChanged(); void isEnabledChanged(); void isDepencyChanged(); void rebakeRequested(); + void fragmentCodeChanged(); + void vertexCodeChanged(); private: void parse(const QString &effectName, const QString &qenPath, const QJsonObject &json); + void ensureShadersCodeEditor(); + void requestRebakeIfLiveUpdateMode(); QString m_name; NodeType m_type = CustomNode; @@ -77,6 +97,7 @@ private: QList m_uniforms; EffectComposerUniformsModel m_unifomrsModel; + Utils::UniqueObjectLatePtr m_shadersCodeEditor; }; } // namespace EffectComposer diff --git a/src/plugins/effectcomposer/effectcodeeditorwidget.cpp b/src/plugins/effectcomposer/effectcodeeditorwidget.cpp new file mode 100644 index 00000000000..c6fcf7c952e --- /dev/null +++ b/src/plugins/effectcomposer/effectcodeeditorwidget.cpp @@ -0,0 +1,142 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "effectcodeeditorwidget.h" + +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include + +namespace EffectComposer { + +constexpr char EFFECTEDITOR_CONTEXT_ID[] = "EffectEditor.EffectEditorContext"; + +EffectCodeEditorWidget::EffectCodeEditorWidget() + : m_context(new Core::IContext(this)) +{ + Core::Context context(EFFECTEDITOR_CONTEXT_ID, ProjectExplorer::Constants::QMLJS_LANGUAGE_ID); + + m_context->setWidget(this); + m_context->setContext(context); + Core::ICore::addContextObject(m_context); + + Utils::TransientScrollAreaSupport::support(this); + + /* + * We have to register our own active auto completion shortcut, because the original shortcut will + * use the cursor position of the original editor in the editor manager. + */ + m_completionAction = new QAction(tr("Trigger Completion"), this); + + Core::Command *command = Core::ActionManager::registerAction( + m_completionAction, TextEditor::Constants::COMPLETE_THIS, context); + command->setDefaultKeySequence(QKeySequence( + Core::useMacShortcuts + ? tr("Meta+Space") + : tr("Ctrl+Space"))); + + connect(m_completionAction, &QAction::triggered, this, [this] { + invokeAssist(TextEditor::Completion); + }); +} + +EffectCodeEditorWidget::~EffectCodeEditorWidget() +{ + unregisterAutoCompletion(); +} + +void EffectCodeEditorWidget::unregisterAutoCompletion() +{ + if (m_completionAction) { + Core::ActionManager::unregisterAction(m_completionAction, TextEditor::Constants::COMPLETE_THIS); + delete m_completionAction; + m_completionAction = nullptr; + } +} + +void EffectCodeEditorWidget::setEditorTextWithIndentation(const QString &text) +{ + auto *doc = document(); + doc->setPlainText(text); + + // We don't need to indent an empty text but is also needed for safer text.length()-1 below + if (text.isEmpty()) + return; + + auto modifier = std::make_unique(doc, QTextCursor{doc}); + modifier->indent(0, text.length()-1); +} + +EffectDocument::EffectDocument() + : QmlJSEditor::QmlJSEditorDocument(EFFECTEDITOR_CONTEXT_ID) + , m_semanticHighlighter(new QmlJSEditor::SemanticHighlighter(this)) +{} + +EffectDocument::~EffectDocument() +{ + delete m_semanticHighlighter; +} + +void EffectDocument::applyFontSettings() +{ + TextDocument::applyFontSettings(); + m_semanticHighlighter->updateFontSettings(fontSettings()); + if (!isSemanticInfoOutdated() && semanticInfo().isValid()) + m_semanticHighlighter->rerun(semanticInfo()); +} + +void EffectDocument::triggerPendingUpdates() +{ + TextDocument::triggerPendingUpdates(); // Calls applyFontSettings if necessary + if (!isSemanticInfoOutdated() && semanticInfo().isValid()) + m_semanticHighlighter->rerun(semanticInfo()); +} + +EffectCodeEditorFactory::EffectCodeEditorFactory() +{ + setId(EFFECTEDITOR_CONTEXT_ID); + setDisplayName(::Core::Tr::tr("Effect Code Editor")); + addMimeType(EFFECTEDITOR_CONTEXT_ID); + addMimeType(Utils::Constants::QML_MIMETYPE); + addMimeType(Utils::Constants::QMLTYPES_MIMETYPE); + addMimeType(Utils::Constants::JS_MIMETYPE); + + setDocumentCreator([]() { return new EffectDocument; }); + setEditorWidgetCreator([]() { return new EffectCodeEditorWidget; }); + setEditorCreator([]() { return new QmlJSEditor::QmlJSEditor; }); + setAutoCompleterCreator([]() { return new QmlJSEditor::AutoCompleter; }); + setCommentDefinition(Utils::CommentDefinition::CppStyle); + setParenthesesMatchingEnabled(true); + setCodeFoldingSupported(true); + + addHoverHandler(new QmlJSEditor::QmlJSHoverHandler); + setCompletionAssistProvider(new QmlJSEditor::QmlJSCompletionAssistProvider); +} + +void EffectCodeEditorFactory::decorateEditor(TextEditor::TextEditorWidget *editor) +{ + editor->textDocument()->resetSyntaxHighlighter( + [] { return new QmlJSEditor::QmlJSHighlighter(); }); + editor->textDocument()->setIndenter(QmlJSEditor::createQmlJsIndenter( + editor->textDocument()->document())); + editor->setAutoCompleter(new QmlJSEditor::AutoCompleter); +} + +} // namespace EffectComposer diff --git a/src/plugins/effectcomposer/effectcodeeditorwidget.h b/src/plugins/effectcomposer/effectcodeeditorwidget.h new file mode 100644 index 00000000000..1ea043dc68e --- /dev/null +++ b/src/plugins/effectcomposer/effectcodeeditorwidget.h @@ -0,0 +1,63 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include +#include + +QT_FORWARD_DECLARE_CLASS(QAction) + +namespace QmlJSEditor { +class SemanticHighlighter; +} + +namespace Core { +class IContext; +} + +namespace EffectComposer { + +class EffectCodeEditorWidget : public QmlJSEditor::QmlJSEditorWidget +{ + Q_OBJECT + +public: + EffectCodeEditorWidget(); + ~EffectCodeEditorWidget() override; + + void unregisterAutoCompletion(); + void setEditorTextWithIndentation(const QString &text); + +signals: + void returnKeyClicked(); + +public: + Core::IContext *m_context = nullptr; + QAction *m_completionAction = nullptr; + bool m_isMultiline = true; +}; + +class EffectDocument : public QmlJSEditor::QmlJSEditorDocument +{ +public: + EffectDocument(); + ~EffectDocument(); + +protected: + void applyFontSettings() final; + void triggerPendingUpdates() final; + +private: + QmlJSEditor::SemanticHighlighter *m_semanticHighlighter = nullptr; +}; + +class EffectCodeEditorFactory : public TextEditor::TextEditorFactory +{ +public: + EffectCodeEditorFactory(); + + static void decorateEditor(TextEditor::TextEditorWidget *editor); +}; + +} // namespace EffectComposer diff --git a/src/plugins/effectcomposer/effectcomposermodel.cpp b/src/plugins/effectcomposer/effectcomposermodel.cpp index 3561f46a3f0..673c6c62b70 100644 --- a/src/plugins/effectcomposer/effectcomposermodel.cpp +++ b/src/plugins/effectcomposer/effectcomposermodel.cpp @@ -4,6 +4,7 @@ #include "effectcomposermodel.h" #include "compositionnode.h" +#include "effectshaderscodeeditor.h" #include "effectutils.h" #include "propertyhandler.h" #include "syntaxhighlighterdata.h" @@ -252,6 +253,8 @@ void EffectComposerModel::setFragmentShader(const QString &newFragmentShader) return; m_fragmentShader = newFragmentShader; + + rebakeIfLiveUpdateMode(); } QString EffectComposerModel::vertexShader() const @@ -265,6 +268,8 @@ void EffectComposerModel::setVertexShader(const QString &newVertexShader) return; m_vertexShader = newVertexShader; + + rebakeIfLiveUpdateMode(); } QString EffectComposerModel::qmlComponentString() const @@ -990,6 +995,50 @@ void EffectComposerModel::saveComposition(const QString &name) setHasUnsavedChanges(false); } +void EffectComposerModel::openShadersCodeEditor(int idx) +{ + if (m_nodes.size() < idx || idx < 0) + return; + + CompositionNode *node = m_nodes.at(idx); + node->openShadersCodeEditor(); +} + +void EffectComposerModel::openMainShadersCodeEditor() +{ + if (!m_shadersCodeEditor) { + m_shadersCodeEditor = Utils::makeUniqueObjectLatePtr( + currentComposition()); + m_shadersCodeEditor->setFragmentValue(generateFragmentShader(true)); + m_shadersCodeEditor->setVertexValue(generateVertexShader(true)); + + connect( + m_shadersCodeEditor.get(), + &EffectShadersCodeEditor::vertexValueChanged, + this, + [this] { + setVertexShader(m_shadersCodeEditor->vertexValue()); + setHasUnsavedChanges(true); + }); + + connect( + m_shadersCodeEditor.get(), + &EffectShadersCodeEditor::fragmentValueChanged, + this, + [this] { + setFragmentShader(m_shadersCodeEditor->fragmentValue()); + setHasUnsavedChanges(true); + }); + + connect( + m_shadersCodeEditor.get(), + &EffectShadersCodeEditor::rebakeRequested, + this, + &EffectComposerModel::startRebakeTimer); + } + m_shadersCodeEditor->showWidget(); +} + void EffectComposerModel::openComposition(const QString &path) { clear(true); @@ -1828,7 +1877,6 @@ void EffectComposerModel::bakeShaders() runQsb(qsbPath, outPaths, false); runQsb(qsbPrevPath, outPrevPaths, true); - } bool EffectComposerModel::shadersUpToDate() const @@ -2003,14 +2051,16 @@ QString EffectComposerModel::getQmlComponentString(bool localFiles) void EffectComposerModel::connectCompositionNode(CompositionNode *node) { - connect(qobject_cast(node->uniformsModel()), - &EffectComposerUniformsModel::dataChanged, this, [this] { - setHasUnsavedChanges(true); - }); - connect(node, &CompositionNode::rebakeRequested, this, [this] { - // This can come multiple times in a row in response to property changes, so let's buffer it - m_rebakeTimer.start(200); - }); + auto setUnsaved = std::bind(&EffectComposerModel::setHasUnsavedChanges, this, true); + connect( + qobject_cast(node->uniformsModel()), + &EffectComposerUniformsModel::dataChanged, + this, + setUnsaved); + + connect(node, &CompositionNode::rebakeRequested, this, &EffectComposerModel::startRebakeTimer); + connect(node, &CompositionNode::fragmentCodeChanged, this, setUnsaved); + connect(node, &CompositionNode::vertexCodeChanged, this, setUnsaved); } void EffectComposerModel::updateExtraMargin() @@ -2020,6 +2070,18 @@ void EffectComposerModel::updateExtraMargin() m_extraMargin = qMax(node->extraMargin(), m_extraMargin); } +void EffectComposerModel::startRebakeTimer() +{ + // This can come multiple times in a row in response to property changes, so let's buffer it + m_rebakeTimer.start(200); +} + +void EffectComposerModel::rebakeIfLiveUpdateMode() +{ + if (m_shadersCodeEditor && m_shadersCodeEditor->liveUpdate()) + startRebakeTimer(); +} + QSet EffectComposerModel::getExposedProperties(const QByteArray &qmlContent) { QSet returnSet; @@ -2051,6 +2113,8 @@ void EffectComposerModel::setCurrentComposition(const QString &newCurrentComposi m_currentComposition = newCurrentComposition; emit currentCompositionChanged(); + + m_shadersCodeEditor.reset(); } Utils::FilePath EffectComposerModel::compositionPath() const diff --git a/src/plugins/effectcomposer/effectcomposermodel.h b/src/plugins/effectcomposer/effectcomposermodel.h index 098cc730069..9079e9bfd35 100644 --- a/src/plugins/effectcomposer/effectcomposermodel.h +++ b/src/plugins/effectcomposer/effectcomposermodel.h @@ -6,6 +6,7 @@ #include "shaderfeatures.h" #include +#include #include #include @@ -26,6 +27,7 @@ class Process; namespace EffectComposer { class CompositionNode; +class EffectShadersCodeEditor; class Uniform; struct EffectError { @@ -100,6 +102,9 @@ public: Q_INVOKABLE void saveComposition(const QString &name); + Q_INVOKABLE void openShadersCodeEditor(int idx); + Q_INVOKABLE void openMainShadersCodeEditor(); + void openComposition(const QString &path); QString currentComposition() const; @@ -190,6 +195,8 @@ private: void connectCompositionNode(CompositionNode *node); void updateExtraMargin(); + void startRebakeTimer(); + void rebakeIfLiveUpdateMode(); QSet getExposedProperties(const QByteArray &qmlContent); QList m_nodes; @@ -230,6 +237,7 @@ private: int m_extraMargin = 0; QString m_effectTypePrefix; Utils::FilePath m_compositionPath; + Utils::UniqueObjectLatePtr m_shadersCodeEditor; const QRegularExpression m_spaceReg = QRegularExpression("\\s+"); }; diff --git a/src/plugins/effectcomposer/effectshaderscodeeditor.cpp b/src/plugins/effectcomposer/effectshaderscodeeditor.cpp new file mode 100644 index 00000000000..95fe30a54e6 --- /dev/null +++ b/src/plugins/effectcomposer/effectshaderscodeeditor.cpp @@ -0,0 +1,226 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "effectshaderscodeeditor.h" +#include "effectcodeeditorwidget.h" + +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace { + +using IconId = QmlDesigner::DesignerIcons::IconId; + +inline constexpr char EFFECTCOMPOSER_LIVE_UPDATE_KEY[] = "EffectComposer/CodeEditor/LiveUpdate"; + +QIcon toolbarIcon(IconId iconId) +{ + return QmlDesigner::DesignerActionManager::instance().toolbarIcon(iconId); +}; + +} // namespace + +namespace EffectComposer { + +EffectShadersCodeEditor::EffectShadersCodeEditor(const QString &title, QWidget *parent) + : QWidget(parent) + , m_settings(new QSettings(qApp->organizationName(), qApp->applicationName(), this)) +{ + setWindowFlag(Qt::Tool, true); + setWindowTitle(title); + + m_fragmentEditor = createJSEditor(); + m_vertexEditor = createJSEditor(); + + connect( + m_fragmentEditor, + &QPlainTextEdit::textChanged, + this, + &EffectShadersCodeEditor::fragmentValueChanged); + connect( + m_vertexEditor, + &QPlainTextEdit::textChanged, + this, + &EffectShadersCodeEditor::vertexValueChanged); + + setupUIComponents(); +} + +EffectShadersCodeEditor::~EffectShadersCodeEditor() +{ + m_fragmentEditor->deleteLater(); + m_vertexEditor->deleteLater(); +} + +void EffectShadersCodeEditor::showWidget() +{ + readAndApplyLiveUpdateSettings(); + show(); + raise(); + m_vertexEditor->setFocus(); +} + +void EffectShadersCodeEditor::showWidget(int x, int y) +{ + showWidget(); + move(QPoint(x, y)); +} + +QString EffectShadersCodeEditor::fragmentValue() const +{ + if (!m_fragmentEditor) + return {}; + + return m_fragmentEditor->document()->toPlainText(); +} + +void EffectShadersCodeEditor::setFragmentValue(const QString &text) +{ + if (m_fragmentEditor) + m_fragmentEditor->setEditorTextWithIndentation(text); +} + +QString EffectShadersCodeEditor::vertexValue() const +{ + if (!m_vertexEditor) + return {}; + + return m_vertexEditor->document()->toPlainText(); +} + +void EffectShadersCodeEditor::setVertexValue(const QString &text) +{ + if (m_vertexEditor) + m_vertexEditor->setEditorTextWithIndentation(text); +} + +bool EffectShadersCodeEditor::liveUpdate() const +{ + return m_liveUpdate; +} + +void EffectShadersCodeEditor::setLiveUpdate(bool liveUpdate) +{ + if (m_liveUpdate == liveUpdate) + return; + + m_liveUpdate = liveUpdate; + writeLiveUpdateSettings(); + + emit liveUpdateChanged(m_liveUpdate); + + if (m_liveUpdate) + emit rebakeRequested(); +} + +EffectCodeEditorWidget *EffectShadersCodeEditor::createJSEditor() +{ + static EffectCodeEditorFactory f; + TextEditor::BaseTextEditor *editor = qobject_cast( + f.createEditor()); + Q_ASSERT(editor); + + editor->setParent(this); + + EffectCodeEditorWidget *editorWidget = qobject_cast( + editor->editorWidget()); + Q_ASSERT(editorWidget); + + editorWidget->setLineNumbersVisible(false); + editorWidget->setMarksVisible(false); + editorWidget->setCodeFoldingSupported(false); + editorWidget->setTabChangesFocus(true); + editorWidget->unregisterAutoCompletion(); + editorWidget->setParent(this); + editorWidget->setFrameStyle(QFrame::StyledPanel | QFrame::Raised); + + return editorWidget; +} + +void EffectShadersCodeEditor::setupUIComponents() +{ + QVBoxLayout *verticalLayout = new QVBoxLayout(this); + QTabWidget *tabWidget = new QTabWidget(this); + + tabWidget->addTab(m_fragmentEditor, tr("Fragment Shader")); + tabWidget->addTab(m_vertexEditor, tr("Vertex Shader")); + + verticalLayout->setContentsMargins(0, 0, 0, 0); + verticalLayout->addWidget(createToolbar()); + verticalLayout->addWidget(tabWidget); + + this->resize(660, 240); +} + +void EffectShadersCodeEditor::closeEvent(QCloseEvent *event) +{ + QWidget::closeEvent(event); + + if (!liveUpdate()) + emit rebakeRequested(); +} + +void EffectShadersCodeEditor::writeLiveUpdateSettings() +{ + m_settings->setValue(EFFECTCOMPOSER_LIVE_UPDATE_KEY, m_liveUpdate); +} + +void EffectShadersCodeEditor::readAndApplyLiveUpdateSettings() +{ + bool liveUpdateStatus = m_settings->value(EFFECTCOMPOSER_LIVE_UPDATE_KEY, false).toBool(); + + setLiveUpdate(liveUpdateStatus); +} + +QToolBar *EffectShadersCodeEditor::createToolbar() +{ + using QmlDesigner::Theme; + + QToolBar *toolbar = new QToolBar(this); + + toolbar->setFixedHeight(Theme::toolbarSize()); + toolbar->setFloatable(false); + toolbar->setContentsMargins(0, 0, 0, 0); + + toolbar->setStyleSheet(Theme::replaceCssColors( + QString::fromUtf8(Utils::FileReader::fetchQrc(":/qmldesigner/stylesheet.css")))); + + QAction *liveUpdateButton + = toolbar->addAction(toolbarIcon(IconId::LiveUpdateIcon), tr("Live Update")); + liveUpdateButton->setCheckable(true); + connect(liveUpdateButton, &QAction::toggled, this, &EffectShadersCodeEditor::setLiveUpdate); + + QAction *applyAction = toolbar->addAction(toolbarIcon(IconId::SyncIcon), tr("Apply")); + connect(applyAction, &QAction::triggered, this, &EffectShadersCodeEditor::rebakeRequested); + + auto syncLive = [liveUpdateButton, applyAction](bool liveState) { + liveUpdateButton->setChecked(liveState); + applyAction->setDisabled(liveState); + }; + + connect(this, &EffectShadersCodeEditor::liveUpdateChanged, this, syncLive); + syncLive(liveUpdate()); + + toolbar->addAction(liveUpdateButton); + toolbar->addAction(applyAction); + + return toolbar; +} + +} // namespace EffectComposer diff --git a/src/plugins/effectcomposer/effectshaderscodeeditor.h b/src/plugins/effectcomposer/effectshaderscodeeditor.h new file mode 100644 index 00000000000..f797315f4a6 --- /dev/null +++ b/src/plugins/effectcomposer/effectshaderscodeeditor.h @@ -0,0 +1,61 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#pragma once + +#include + +QT_FORWARD_DECLARE_CLASS(QSettings) +QT_FORWARD_DECLARE_CLASS(QToolBar) + +namespace EffectComposer { + +class EffectCodeEditorWidget; + +class EffectShadersCodeEditor : public QWidget +{ + Q_OBJECT + Q_PROPERTY(bool liveUpdate READ liveUpdate WRITE setLiveUpdate NOTIFY liveUpdateChanged) + +public: + EffectShadersCodeEditor(const QString &title = tr("Untitled Editor"), QWidget *parent = nullptr); + ~EffectShadersCodeEditor() override; + + void showWidget(); + void showWidget(int x, int y); + + QString fragmentValue() const; + void setFragmentValue(const QString &text); + + QString vertexValue() const; + void setVertexValue(const QString &text); + + bool liveUpdate() const; + void setLiveUpdate(bool liveUpdate); + +signals: + void liveUpdateChanged(bool); + void fragmentValueChanged(); + void vertexValueChanged(); + void rebakeRequested(); + +protected: + using QWidget::show; + EffectCodeEditorWidget *createJSEditor(); + void setupUIComponents(); + + void closeEvent(QCloseEvent *event) override; + +private: + void writeLiveUpdateSettings(); + void readAndApplyLiveUpdateSettings(); + QToolBar *createToolbar(); + + QSettings *m_settings = nullptr; + QPointer m_fragmentEditor; + QPointer m_vertexEditor; + + bool m_liveUpdate = false; +}; + +} // namespace EffectComposer diff --git a/src/plugins/qmldesigner/components/bindingeditor/bindingeditorwidget.cpp b/src/plugins/qmldesigner/components/bindingeditor/bindingeditorwidget.cpp index 07497c935e0..652f3838539 100644 --- a/src/plugins/qmldesigner/components/bindingeditor/bindingeditorwidget.cpp +++ b/src/plugins/qmldesigner/components/bindingeditor/bindingeditorwidget.cpp @@ -21,7 +21,6 @@ #include -#include #include #include diff --git a/src/plugins/qmldesigner/components/componentcore/designericons.h b/src/plugins/qmldesigner/components/componentcore/designericons.h index 1bce6581bdb..98edfac0b59 100644 --- a/src/plugins/qmldesigner/components/componentcore/designericons.h +++ b/src/plugins/qmldesigner/components/componentcore/designericons.h @@ -80,6 +80,7 @@ public: LightDirectionalIcon, LightPointIcon, LightSpotIcon, + LiveUpdateIcon, LocalOrientIcon, MakeComponentIcon, MaterialIcon, @@ -107,6 +108,7 @@ public: SnappingIcon, SnappingConfIcon, SplitViewIcon, + SyncIcon, TimelineIcon, ToggleGroupIcon, VisibilityIcon diff --git a/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.cpp b/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.cpp index 4f5bdab700d..950575888e6 100644 --- a/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.cpp +++ b/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.cpp @@ -1759,13 +1759,10 @@ void editInEffectComposer(const SelectionContext &selectionContext) bool isEffectComposerActivated() { - const ExtensionSystem::PluginSpecs specs = ExtensionSystem::PluginManager::plugins(); - return std::ranges::find_if(specs, - [](ExtensionSystem::PluginSpec *spec) { - return spec->name() == "EffectComposer" - && spec->isEffectivelyEnabled(); - }) - != specs.end(); + using namespace ExtensionSystem; + return Utils::anyOf(PluginManager::plugins(), [](PluginSpec *spec) { + return spec->name() == "EffectComposer" && spec->isEffectivelyEnabled(); + }); } void openEffectComposer(const QString &filePath)