From 5368330534081ca2ff9788f37aa17e762a325c20 Mon Sep 17 00:00:00 2001 From: Ali Kianian Date: Thu, 29 Dec 2022 22:42:30 +0200 Subject: [PATCH] QmlDesigner: Make context menu grouping a single action - Grouping actions has been merged into one checkable item. - range and view namespaces are available for Utils. Task-number: QDS-8662 Change-Id: I17b2190caf425d0d8271dd68af494dbc2b332f09 Reviewed-by: Mahmoud Badri Reviewed-by: Miikka Heikkinen Reviewed-by: Thomas Hartmann --- src/libs/utils/CMakeLists.txt | 1 + src/libs/utils/ranges.h | 95 +++++++++++ src/plugins/qmldesigner/CMakeLists.txt | 1 + .../componentcore/componentcore_constants.h | 1 - .../componentcore/designeractionmanager.cpp | 27 +-- .../componentcore/groupitemaction.cpp | 155 ++++++++++++++++++ .../componentcore/groupitemaction.h | 23 +++ .../designercore/include/modelnode.h | 1 + .../designercore/model/modelnode.cpp | 26 +++ 9 files changed, 306 insertions(+), 24 deletions(-) create mode 100644 src/libs/utils/ranges.h create mode 100644 src/plugins/qmldesigner/components/componentcore/groupitemaction.cpp create mode 100644 src/plugins/qmldesigner/components/componentcore/groupitemaction.h diff --git a/src/libs/utils/CMakeLists.txt b/src/libs/utils/CMakeLists.txt index a4135401651..a9155818c56 100644 --- a/src/libs/utils/CMakeLists.txt +++ b/src/libs/utils/CMakeLists.txt @@ -132,6 +132,7 @@ add_qtc_library(Utils qtcolorbutton.cpp qtcolorbutton.h qtcprocess.cpp qtcprocess.h qtcsettings.cpp qtcsettings.h + ranges.h reloadpromptutils.cpp reloadpromptutils.h removefiledialog.cpp removefiledialog.h runextensions.cpp runextensions.h diff --git a/src/libs/utils/ranges.h b/src/libs/utils/ranges.h new file mode 100644 index 00000000000..8b574e08d6f --- /dev/null +++ b/src/libs/utils/ranges.h @@ -0,0 +1,95 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +#pragma once + +#if __cplusplus >= 202002L +#include + +namespace Utils { + +namespace ranges { +using std::ranges::reverse_view; +} + +namespace views { +using std::views::reverse; +} + +} // namespace Utils +#else +#include + +namespace Utils { + +namespace ranges { + +template +class reverse_view +{ +public: + using value_type = typename Container::value_type; + using size_type = typename Container::size_type; + using Type = value_type; + using pointer = Type *; + using const_pointer = const Type *; + using reference = Type &; + using const_reference = const Type &; + + using reverse_iterator = typename Container::iterator; + using iterator = std::reverse_iterator; + + using const_reverse_iterator = typename Container::const_iterator; + using const_iterator = std::reverse_iterator; + + using Iterator = iterator; + using ConstIterator = const_iterator; + + reverse_view(const Container &k) : d(&k) {} + + const_reverse_iterator rbegin() const noexcept { return d->begin(); } + const_reverse_iterator rend() const noexcept { return d->end(); } + const_reverse_iterator crbegin() const noexcept { return d->begin(); } + const_reverse_iterator crend() const noexcept { return d->end(); } + + const_iterator begin() const noexcept { return const_iterator(rend()); } + const_iterator end() const noexcept { return const_iterator(rbegin()); } + const_iterator cbegin() const noexcept { return const_iterator(rend()); } + const_iterator cend() const noexcept { return const_iterator(rbegin()); } + const_iterator constBegin() const noexcept { return const_iterator(rend()); } + const_iterator constEnd() const noexcept { return const_iterator(rbegin()); } + + const_reference front() const noexcept { return *cbegin(); } + const_reference back() const noexcept { return *crbegin(); } + + [[nodiscard]] size_type size() const noexcept { return d->size(); } + [[nodiscard]] bool empty() const noexcept { return d->size() == 0; } + explicit operator bool() const { return d->size(); } + + const_reference operator[](size_type idx) const + { + if (idx < size()) + return *(begin() + idx); + + throw std::out_of_range("bad index in reverse side"); + } + +private: + const Container *d; +}; +} // namespace ranges + +namespace views { + +constexpr struct {} reverse; + +template +inline ranges::reverse_view operator|(const T &container, const decltype(reverse)&) +{ + return ranges::reverse_view(container); +} + +} // namsepace views +} // namespace Utils + +#endif diff --git a/src/plugins/qmldesigner/CMakeLists.txt b/src/plugins/qmldesigner/CMakeLists.txt index a58841b1204..811bcad4782 100644 --- a/src/plugins/qmldesigner/CMakeLists.txt +++ b/src/plugins/qmldesigner/CMakeLists.txt @@ -614,6 +614,7 @@ extend_qtc_plugin(QmlDesigner designeractionmanagerview.cpp designeractionmanagerview.h designericons.cpp designericons.h findimplementation.cpp findimplementation.h + groupitemaction.cpp groupitemaction.h layoutingridlayout.cpp layoutingridlayout.h modelnodecontextmenu.cpp modelnodecontextmenu.h modelnodecontextmenu_helper.cpp modelnodecontextmenu_helper.h diff --git a/src/plugins/qmldesigner/components/componentcore/componentcore_constants.h b/src/plugins/qmldesigner/components/componentcore/componentcore_constants.h index 3f61fde48ed..1d89bb37e73 100644 --- a/src/plugins/qmldesigner/components/componentcore/componentcore_constants.h +++ b/src/plugins/qmldesigner/components/componentcore/componentcore_constants.h @@ -75,7 +75,6 @@ const char decreaseIndexOfStackedContainerCommandId[] = "DecreaseIndexOfStackedC const char flowAssignEffectCommandId[] = "AssignFlowEffect"; const char flowAssignCustomEffectCommandId[] = "AssignFlowCustomEffect"; const char addToGroupItemCommandId[] = "AddToGroupItem"; -const char removeGroupItemCommandId[] = "RemoveToGroupItem"; const char fitRootToScreenCommandId[] = "FitRootToScreen"; const char fitSelectionToScreenCommandId[] = "FitSelectionToScreen"; const char editAnnotationsCommandId[] = "EditAnnotation"; diff --git a/src/plugins/qmldesigner/components/componentcore/designeractionmanager.cpp b/src/plugins/qmldesigner/components/componentcore/designeractionmanager.cpp index bbe86b5e0e3..a0bf2fbd0e4 100644 --- a/src/plugins/qmldesigner/components/componentcore/designeractionmanager.cpp +++ b/src/plugins/qmldesigner/components/componentcore/designeractionmanager.cpp @@ -9,6 +9,7 @@ #include "designericons.h" #include "designermcumanager.h" #include "formatoperation.h" +#include "groupitemaction.h" #include "modelnodecontextmenu_helper.h" #include "qmldesignerconstants.h" #include "qmleditormenu.h" @@ -1593,12 +1594,10 @@ void DesignerActionManager::createDefaultDesignerActions() &selectionEnabled, &selectionEnabled)); - addDesignerAction(new ActionGroup( - groupCategoryDisplayName, - groupCategory, + addDesignerAction(new GroupItemAction( contextIcon(DesignerIcons::GroupSelectionIcon), - Priorities::Group, - &studioComponentsAvailableAndSelectionCanBeLayouted)); + {}, + Priorities::Group)); addDesignerAction(new ActionGroup( flowCategoryDisplayName, @@ -1745,24 +1744,6 @@ void DesignerActionManager::createDefaultDesignerActions() &isLayout, &isLayout)); - addDesignerAction(new ModelNodeContextMenuAction(addToGroupItemCommandId, - addToGroupItemDisplayName, - {}, - groupCategory, - QKeySequence("Ctrl+Shift+g"), - 1, - &addToGroupItem, - &selectionCanBeLayouted)); - - addDesignerAction(new ModelNodeContextMenuAction(removeGroupItemCommandId, - removeGroupItemDisplayName, - {}, - groupCategory, - QKeySequence(), - 2, - &removeGroup, - &isGroup)); - addDesignerAction(new ModelNodeFormEditorAction( addItemToStackedContainerCommandId, addItemToStackedContainerDisplayName, diff --git a/src/plugins/qmldesigner/components/componentcore/groupitemaction.cpp b/src/plugins/qmldesigner/components/componentcore/groupitemaction.cpp new file mode 100644 index 00000000000..f60e0ee5e33 --- /dev/null +++ b/src/plugins/qmldesigner/components/componentcore/groupitemaction.cpp @@ -0,0 +1,155 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +#include "groupitemaction.h" + +#include "nodeabstractproperty.h" +#include "nodelistproperty.h" + +#include + +using namespace QmlDesigner; + +namespace { + +bool selectionsAreSiblings(const QList &selectedItems) +{ + const QList prunedSelectedItems = ModelNode::pruneChildren(selectedItems); + if (prunedSelectedItems.size() < 2) + return false; + + ModelNode modelItemNode(prunedSelectedItems.first()); + if (!modelItemNode.isValid()) + return false; + + ModelNode parentNode = modelItemNode.parentProperty().parentModelNode(); + if (!parentNode.isValid()) + return false; + + for (const ModelNode &node : Utils::span(prunedSelectedItems).subspan(1)) { + if (!node.isValid()) + return false; + + if (node.parentProperty().parentModelNode() != parentNode) + return false; + } + + return true; +} + +ModelNode availableGroupNode(const SelectionContext &selection) +{ + if (!selection.isValid()) + return {}; + + if (selection.singleNodeIsSelected()) { + const ModelNode node = selection.currentSingleSelectedNode(); + if (node.metaInfo().isQtQuickStudioComponentsGroupItem()) + return node; + } + + const ModelNode parentNode = selection + .firstSelectedModelNode() + .parentProperty().parentModelNode(); + + if (!parentNode.isValid()) + return {}; + + QList allSiblingNodes = parentNode.directSubModelNodes(); + + QList selectedNodes = ModelNode::pruneChildren(selection.selectedModelNodes()); + if (selectedNodes.size() != allSiblingNodes.size()) + return {}; + + Utils::sort(allSiblingNodes); + Utils::sort(selectedNodes); + + if (allSiblingNodes != selectedNodes) + return {}; + + if (parentNode.metaInfo().isQtQuickStudioComponentsGroupItem()) + return parentNode; + + return {}; +} + +inline bool itemsAreGrouped(const SelectionContext &selection) +{ + return availableGroupNode(selection).isValid(); +} + +bool groupingEnabled(const SelectionContext &selection) +{ + if (selection.singleNodeIsSelected()) + return itemsAreGrouped(selection); + else + return selectionsAreSiblings(selection.selectedModelNodes()); +} + +void removeGroup(const ModelNode &group) +{ + QmlItemNode groupItem(group); + QmlItemNode parent = groupItem.instanceParentItem(); + + if (!groupItem || !parent) + return; + + group.view()->executeInTransaction( + "removeGroup", [group, &groupItem, parent]() { + for (const ModelNode &modelNode : group.directSubModelNodes()) { + if (QmlVisualNode qmlItem = modelNode) { + QPointF pos = qmlItem.position().toPointF(); + pos = groupItem.instanceTransform().map(pos); + qmlItem.setPosition(pos); + + parent.modelNode().defaultNodeListProperty().reparentHere(modelNode); + } + } + groupItem.destroy(); + }); +} + +void toggleGrouping(const SelectionContext &selection) +{ + if (!selection.isValid()) + return; + + ModelNode groupNode = availableGroupNode(selection); + + if (groupNode.isValid()) + removeGroup(groupNode); + else + ModelNodeOperations::addToGroupItem(selection); +} + +} // blank namespace + +GroupItemAction::GroupItemAction(const QIcon &icon, + const QKeySequence &key, + int priority) + : ModelNodeAction(ComponentCoreConstants::addToGroupItemCommandId, + {}, + icon, + {}, + {}, + key, + priority, + &toggleGrouping, + &groupingEnabled) +{ + setCheckable(true); +} + +void GroupItemAction::updateContext() +{ + using namespace ComponentCoreConstants; + ModelNodeAction::updateContext(); + action()->setText(QString::fromLatin1(action()->isChecked() + ? removeGroupItemDisplayName + : addToGroupItemDisplayName)); +} + +bool GroupItemAction::isChecked(const SelectionContext &selectionContext) const +{ + return itemsAreGrouped(selectionContext); +} diff --git a/src/plugins/qmldesigner/components/componentcore/groupitemaction.h b/src/plugins/qmldesigner/components/componentcore/groupitemaction.h new file mode 100644 index 00000000000..8d217a87190 --- /dev/null +++ b/src/plugins/qmldesigner/components/componentcore/groupitemaction.h @@ -0,0 +1,23 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 + +#pragma once + +#include + +namespace QmlDesigner { + +class GroupItemAction: public ModelNodeAction +{ +public: + GroupItemAction(const QIcon &icon, + const QKeySequence &key, + int priority); + + +protected: + virtual void updateContext() override; + virtual bool isChecked(const SelectionContext &selectionContext) const override; +}; + +} diff --git a/src/plugins/qmldesigner/designercore/include/modelnode.h b/src/plugins/qmldesigner/designercore/include/modelnode.h index c9c78588004..bcd6c55f258 100644 --- a/src/plugins/qmldesigner/designercore/include/modelnode.h +++ b/src/plugins/qmldesigner/designercore/include/modelnode.h @@ -223,6 +223,7 @@ public: static bool isThisOrAncestorLocked(const ModelNode &node); static ModelNode lowestCommonAncestor(const QList &nodes); + static QList pruneChildren(const QList &nodes); qint32 internalId() const; diff --git a/src/plugins/qmldesigner/designercore/model/modelnode.cpp b/src/plugins/qmldesigner/designercore/model/modelnode.cpp index a58e6c60079..8e7a58fc0d5 100644 --- a/src/plugins/qmldesigner/designercore/model/modelnode.cpp +++ b/src/plugins/qmldesigner/designercore/model/modelnode.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -1402,6 +1403,31 @@ ModelNode ModelNode::lowestCommonAncestor(const QList &nodes) return accumulatedNode; } +QList ModelNode::pruneChildren(const QList &nodes) +{ + QList forwardNodes; + QList backNodes; + + auto pushIfIsNotChild = [] (QList &container, const ModelNode &node) { + bool hasAncestor = Utils::anyOf(container, + [node] (const ModelNode &testNode) -> bool { + return testNode.isAncestorOf(node); + }); + if (!hasAncestor) + container.append(node); + }; + + for (const ModelNode &node : nodes | Utils::views::reverse) { + if (node) + pushIfIsNotChild(forwardNodes, node); + } + + for (const ModelNode &node : forwardNodes | Utils::views::reverse) + pushIfIsNotChild(backNodes, node); + + return backNodes; +} + void ModelNode::setScriptFunctions(const QStringList &scriptFunctionList) { model()->d->setScriptFunctions(m_internalNode, scriptFunctionList);