From cab05e015079888e5cd3a501e9d5b6d23facfc96 Mon Sep 17 00:00:00 2001 From: mpv Date: Wed, 25 May 2022 15:07:42 +0300 Subject: [PATCH] #29481: Show parts in study tree (parts drag and drop) --- src/BuildPlugin/BuildPlugin_Interpolation.cpp | 1 - src/ModelAPI/ModelAPI_Tools.cpp | 123 +++++++++++++ src/ModelAPI/ModelAPI_Tools.h | 10 ++ src/ModelAPI/Test/TestMovePart1.py | 125 ++++++++++++++ src/ModelAPI/Test/TestMovePart2.py | 113 ++++++++++++ src/ModelAPI/tests.set | 2 + src/ModuleBase/ModuleBase_Tools.cpp | 30 ---- src/ModuleBase/ModuleBase_WidgetLabel.cpp | 5 +- src/ModuleBase/ModuleBase_WidgetSwitch.cpp | 5 +- src/PartSetPlugin/doc/PartSetPlugin.rst | 15 ++ src/XGUI/XGUI_DataModel.cpp | 162 +++++++++++++++++- src/XGUI/XGUI_DataModel.h | 87 ++-------- src/XGUI/XGUI_MenuMgr.cpp | 4 +- src/XGUI/XGUI_ObjectsBrowser.cpp | 7 + src/XGUI/XGUI_msg_fr.ts | 12 ++ 15 files changed, 586 insertions(+), 115 deletions(-) create mode 100644 src/ModelAPI/Test/TestMovePart1.py create mode 100644 src/ModelAPI/Test/TestMovePart2.py diff --git a/src/BuildPlugin/BuildPlugin_Interpolation.cpp b/src/BuildPlugin/BuildPlugin_Interpolation.cpp index 13cdab609..57b10ea30 100644 --- a/src/BuildPlugin/BuildPlugin_Interpolation.cpp +++ b/src/BuildPlugin/BuildPlugin_Interpolation.cpp @@ -121,7 +121,6 @@ void BuildPlugin_Interpolation::attributeChanged(const std::string& theID) //================================================================================================= void BuildPlugin_Interpolation::updateCoordinates() { - std::wstring exp; double aMint = real(MINT_ID())->value(); double aMaxt = real(MAXT_ID())->value(); int aNbrStep = integer(NUMSTEP_ID())->value(); diff --git a/src/ModelAPI/ModelAPI_Tools.cpp b/src/ModelAPI/ModelAPI_Tools.cpp index dda65e007..703404069 100644 --- a/src/ModelAPI/ModelAPI_Tools.cpp +++ b/src/ModelAPI/ModelAPI_Tools.cpp @@ -1441,4 +1441,127 @@ void findRandomColor(std::vector& theValues, bool theReset) // LCOV_EXCL_STOP +/// Returns name of the higher level feature (Part or feature of PartSet). +static FeaturePtr topOwner(const FeaturePtr& theFeature) +{ + FeaturePtr anOwner = theFeature; + while (anOwner.get()) + { + FeaturePtr aNextOwner = compositeOwner(anOwner); + if (aNextOwner.get()) + anOwner = aNextOwner; + else + break; + } + if (anOwner->document() != ModelAPI_Session::get()->moduleDocument()) // the part-owner name + anOwner = findPartFeature(ModelAPI_Session::get()->moduleDocument(), anOwner->document()); + return anOwner; +} + +std::wstring validateMovement(const FeaturePtr& theAfter, const std::list theMoved) +{ + std::wstring aResult; + if (theMoved.empty()) + return aResult; // nothing to move, nothing to check, ok + DocumentPtr aDoc = theAfter.get() ? theAfter->document() : (*(theMoved.cbegin()))->document(); + std::set aMoved(theMoved.begin(), theMoved.end()); // fast access to moved + std::set aPassed, aPassedMoved; // all features and all moved before the current one + std::set aPassedAfter; // all passed features after theAfter + bool anAfterIsPassed = theAfter.get() == 0; // flag that iterator already passed theAfter + std::list allFeat = aDoc->allFeatures(); + for (std::list::iterator aFeat = allFeat.begin(); aFeat != allFeat.end(); aFeat++) + { + if (!anAfterIsPassed) + { + if (aMoved.count(*aFeat)) + aPassedMoved.insert(*aFeat); + else // check aPassedMoved are not referenced by the current feature + aPassed.insert(*aFeat); + + anAfterIsPassed = *aFeat == theAfter; + if (anAfterIsPassed && !aPassedMoved.empty()) + { // check dependencies of moved relatively to the passed + std::map > aReferences; + findAllReferences(aPassedMoved, aReferences); + std::map >::iterator aRefIter = aReferences.begin(); + for (; aRefIter != aReferences.end(); aRefIter++) + { + if (aPassed.count(aRefIter->first)) + { + aResult += topOwner(aRefIter->first)->name() + L" -> "; + // iterate all passed moved to check is it referenced by described feature or not + std::set::iterator aMovedIter = aPassedMoved.begin(); + for (; aMovedIter != aPassedMoved.end(); aMovedIter++) + { + std::map > aPassedRefs; + std::set aMovedOne; + aMovedOne.insert(*aMovedIter); + findAllReferences(aMovedOne, aPassedRefs); + if (aPassedRefs.count(aRefIter->first)) + aResult += topOwner(*aMovedIter)->name() + L" "; + } + aResult += L"\n"; + } + } + } + } + else // iteration after theAfter + { + if (aMoved.count(*aFeat)) { // check dependencies of moved relatively to ones after theAfter + std::map > aReferences; + findAllReferences(aPassedAfter, aReferences); + bool aFoundRef = aReferences.find(*aFeat) != aReferences.end(); + if (!aFoundRef && !(*aFeat)->results().empty()) // reference may be a feature in moved part + { + ResultPartPtr aFeatPart = + std::dynamic_pointer_cast((*aFeat)->firstResult()); + if (aFeatPart.get() && aFeatPart->partDoc().get()) + { + std::map >::iterator aRef = aReferences.begin(); + for (; aRef != aReferences.end() && !aFoundRef; aRef++) + aFoundRef = aRef->first->document() == aFeatPart->partDoc(); + } + } + + if (aFoundRef) + { + aResult += topOwner(*aFeat)->name() + L" -> "; + std::set aReferencedCount; // to avoid duplicates in the displayed references + // iterate all passed after theAfter to check refers it described feature or not + FeaturePtr aFeatTop = topOwner(*aFeat); + std::set::iterator aPassedIter = aPassedAfter.begin(); + for (; aPassedIter != aPassedAfter.end(); aPassedIter++) + { + FeaturePtr aPassedTop = topOwner(*aPassedIter); + if (aReferencedCount.count(aPassedTop)) + continue; + std::map > aPassedRefs; + std::set aPassedOne; + aPassedOne.insert(*aPassedIter); + findAllReferences(aPassedOne, aPassedRefs); + std::map >::iterator aPRIter = aPassedRefs.begin(); + for (; aPRIter != aPassedRefs.end(); aPRIter++) + { + FeaturePtr aPRTop = topOwner(aPRIter->first); + if (aPRIter->first == *aFeat || aPRIter->first == aFeatTop || + aPRTop == *aFeat || aPRTop == aFeatTop) + { + aResult += aPassedTop->name() + L" "; + aReferencedCount.insert(aPassedTop); + break; + } + } + } + aResult += L"\n"; + } + } + else { + aPassedAfter.insert(*aFeat); + } + } + + } + return aResult; +} + } // namespace ModelAPI_Tools diff --git a/src/ModelAPI/ModelAPI_Tools.h b/src/ModelAPI/ModelAPI_Tools.h index 6f61423e8..0ad692341 100644 --- a/src/ModelAPI/ModelAPI_Tools.h +++ b/src/ModelAPI/ModelAPI_Tools.h @@ -356,6 +356,16 @@ MODELAPI_EXPORT bool isInResults(AttributeSelectionListPtr theSelection, */ MODELAPI_EXPORT void findRandomColor(std::vector& theValues, bool theReset = false); +/*! +* Checks the movement of features possibility. The feature cannot appear before the feature +* depended on it. Used in drag and drop part features. +* \param theAfter feature after which the moved features are placed, or null for the first place +* \param theMoved ordered list of the moved features +* \returns string with error text, dependencies that do not allow make movement or empty string +*/ +MODELAPI_EXPORT std::wstring validateMovement( + const FeaturePtr& theAfter, const std::list theMoved); + } #endif diff --git a/src/ModelAPI/Test/TestMovePart1.py b/src/ModelAPI/Test/TestMovePart1.py new file mode 100644 index 000000000..f05d0e91c --- /dev/null +++ b/src/ModelAPI/Test/TestMovePart1.py @@ -0,0 +1,125 @@ +# Copyright (C) 2014-2021 CEA/DEN, EDF R&D +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +from salome.shaper import model + +model.begin() +partSet = model.moduleDocument() + +### Create Sketch_1 +Sketch_1 = model.addSketch(partSet, model.defaultPlane("XOY")) +SketchProjection_1 = Sketch_1.addProjection(model.selection("VERTEX", "Origin"), False) +SketchPoint_1 = SketchProjection_1.createdFeature() +SketchCircle_1 = Sketch_1.addCircle(0, 0, 50) +Sketch_1.setCoincident(SketchPoint_1.result(), SketchCircle_1.center()) +model.do() + +### Create Sketch_2 with reference to Sketch_1 circle +Sketch_2 = model.addSketch(partSet, model.defaultPlane("XOY")) +SketchProjection_2 = Sketch_2.addProjection(model.selection("EDGE", "Sketch_1/SketchCircle_1_2"), False) +SketchCircle_2 = SketchProjection_2.createdFeature() +SketchCircle_3 = Sketch_2.addCircle(34.92852513931564, 34.57180354201876, 20) +Sketch_2.setCoincident(SketchCircle_2.results()[1], SketchCircle_3.center()) +model.do() + +### Create Part_1: Extrusion of Sketch_1 +Part_1 = model.addPart(partSet) +Part_1_doc = Part_1.document() +Extrusion_1 = model.addExtrusion(Part_1_doc, [model.selection("FACE", "PartSet/Sketch_1/Face-SketchCircle_1_2f")], model.selection(), 10, 0, "Faces|Wires") +model.do() + +### Create Part_2: Extrusion of Sketch_2 +Part_2 = model.addPart(partSet) +Part_2_doc = Part_2.document() +Extrusion_2 = model.addExtrusion(Part_2_doc, [model.selection("FACE", "PartSet/Sketch_2/Face-SketchCircle_3_2f")], model.selection(), 20, 0, "Faces|Wires") +model.do() + +### Create Placement of Part_2 over a Part_1 +Placement_1 = model.addPlacement(partSet, [model.selection("COMPOUND", "Part_2/")], model.selection("FACE", "Part_2/Extrusion_1_1/From_Face"), model.selection("FACE", "Part_1/Extrusion_1_1/To_Face"), centering = True, keepSubResults = True) + +### Create independent Part_3 +Part_3 = model.addPart(partSet) +Part_3_doc = Part_3.document() +Sketch_3 = model.addSketch(Part_3_doc, model.defaultPlane("XOY")) +SketchCircle_4 = Sketch_3.addCircle(-60, 65, 10) +model.do() +Extrusion_3 = model.addExtrusion(Part_3_doc, [model.selection("FACE", "Sketch_1/Face-SketchCircle_1_2r")], model.selection(), 10, 0, "Faces|Wires") +model.do() + +model.end() + +# test the movement of parts ability and produced errors + +# checking ability to move theWhat to position after theTo and produced error theError +def checkMovement(theWhat, theTo, theError): + aWhatList = model.FeatureList() + aWhatList.append(theWhat.feature()) + if not theTo is None and not isinstance(theTo, model.ModelAPI_Object): + theTo = theTo.feature() + anError = model.validateMovement(theTo, aWhatList).strip() + aListCheck = theError.split('\n') + aListGet = anError.split('\n') + assert(len(aListCheck) == len(aListGet)) + for anErr in aListCheck: + #print("Looking for " + anErr + " in " + str(aListGet)) + assert(aListGet.count(anErr) == 1) + +# movement to very first location actually means movement after the first invisible objects, coordinate system hidden ones +aLastHidden = None +for aFeat in partSet.allObjects(): + if aFeat.isInHistory(): + break + aLastHidden = model.objectToFeature(aFeat) + +checkMovement(Part_1, None, 'Part_1 -> Origin Sketch_1') +checkMovement(Part_1, aLastHidden, 'Part_1 -> Sketch_1') +checkMovement(Part_1, Sketch_1, 'Part_1 -> Sketch_1') +checkMovement(Part_1, Sketch_2, '') +checkMovement(Part_1, Part_2, '') +checkMovement(Part_1, Placement_1, 'Placement_1 -> Part_1') +checkMovement(Part_1, Part_3, 'Placement_1 -> Part_1') + +checkMovement(Part_2, None, 'Part_2 -> Origin Sketch_1 Sketch_2') +checkMovement(Part_2, aLastHidden, 'Part_2 -> Sketch_1 Sketch_2') +checkMovement(Part_2, Sketch_1, 'Part_2 -> Sketch_2 Sketch_1') # in GUI this is not the case, because it drops after invisible sketch components +checkMovement(Part_2, Sketch_2, 'Part_2 -> Sketch_2') # in GUI this is not the case, because it drops after invisible sketch components +checkMovement(Part_2, Part_1, '') +checkMovement(Part_2, Placement_1, 'Placement_1 -> Part_2') +checkMovement(Part_2, Part_3, 'Placement_1 -> Part_2') + +checkMovement(Part_3, None, '') +checkMovement(Part_3, aLastHidden, '') +checkMovement(Part_3, Sketch_1, '') +checkMovement(Part_3, Sketch_2, '') +checkMovement(Part_3, Part_1, '') +checkMovement(Part_3, Part_2, '') +checkMovement(Part_3, Placement_1, '') + +# do movement Part_2 after Sketch_2 +anAfterSketch_2 = partSet.nextFeature(Part_1.feature(), True) +model.begin() +partSet.moveFeature(Part_2.feature(), anAfterSketch_2) +model.end() +# check the movement is performed +anAfter = partSet.nextFeature(anAfterSketch_2) +assert(anAfter.name() == 'Part_2') +anAfter = partSet.nextFeature(anAfter) +assert(anAfter.name() == 'Part_1') +anAfter = partSet.nextFeature(anAfter) +assert(anAfter.name() == 'Placement_1') diff --git a/src/ModelAPI/Test/TestMovePart2.py b/src/ModelAPI/Test/TestMovePart2.py new file mode 100644 index 000000000..cc4cb7420 --- /dev/null +++ b/src/ModelAPI/Test/TestMovePart2.py @@ -0,0 +1,113 @@ +# Copyright (C) 2014-2021 CEA/DEN, EDF R&D +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com +# + +from salome.shaper import model + +model.begin() +partSet = model.moduleDocument() + +### Create Part_1 : biggest cylinder +Part_1 = model.addPart(partSet) +Part_1_doc = Part_1.document() +Sketch_1 = model.addSketch(Part_1_doc, model.defaultPlane("XOY")) +SketchProjection_1 = Sketch_1.addProjection(model.selection("VERTEX", "PartSet/Origin"), False) +SketchPoint_1 = SketchProjection_1.createdFeature() +SketchCircle_1 = Sketch_1.addCircle(0, 0, 50) +Sketch_1.setCoincident(SketchPoint_1.result(), SketchCircle_1.center()) +model.do() +Extrusion_1 = model.addExtrusion(Part_1_doc, [model.selection("FACE", "Sketch_1/Face-SketchCircle_1_2f")], model.selection(), 10, 0, "Faces|Wires") +model.do() + +### Create Plane_4 and Sketch_2, independent +Plane_4 = model.addPlane(partSet, model.selection("FACE", "XOY"), 10, False) +Sketch_2 = model.addSketch(partSet, model.selection("FACE", "Plane_4")) +SketchCircle_2 = Sketch_2.addCircle(0, 0, 20) +model.do() + +### Create Part_2, an edge on Sketch_2 +Part_2 = model.addPart(partSet) +Part_2_doc = Part_2.document() +Edge_1 = model.addEdge(Part_2_doc, [model.selection("EDGE", "PartSet/Sketch_1/SketchCircle_1_2")], False) +model.do() + +### Create Part_3, small cylinder on a Sketch_2 +Part_3 = model.addPart(partSet) +Part_3_doc = Part_3.document() +Extrusion_2 = model.addExtrusion(Part_3_doc, [model.selection("FACE", "PartSet/Sketch_1/Face-SketchCircle_1_2r")], model.selection(), 10, 0, "Faces|Wires") +model.do() + +### Create Part_4, a box, independent +Part_4 = model.addPart(partSet) +Part_4_doc = Part_4.document() +Sketch_3 = model.addSketch(Part_4_doc, model.defaultPlane("XOY")) +SketchLine_1 = Sketch_3.addLine(70, 60, 20, 60) +SketchLine_2 = Sketch_3.addLine(20, 60, 20, 30) +SketchLine_3 = Sketch_3.addLine(20, 30, 70, 30) +SketchLine_4 = Sketch_3.addLine(70, 30, 70, 60) +Sketch_3.setCoincident(SketchLine_4.endPoint(), SketchLine_1.startPoint()) +Sketch_3.setCoincident(SketchLine_1.endPoint(), SketchLine_2.startPoint()) +Sketch_3.setCoincident(SketchLine_2.endPoint(), SketchLine_3.startPoint()) +Sketch_3.setCoincident(SketchLine_3.endPoint(), SketchLine_4.startPoint()) +Sketch_3.setHorizontal(SketchLine_1.result()) +Sketch_3.setVertical(SketchLine_2.result()) +Sketch_3.setHorizontal(SketchLine_3.result()) +Sketch_3.setVertical(SketchLine_4.result()) +model.do() +Extrusion_3 = model.addExtrusion(Part_4_doc, [model.selection("FACE", "Sketch_1/Face-SketchLine_1r-SketchLine_2f-SketchLine_3f-SketchLine_4f")], model.selection(), 10, 0, "Faces|Wires") +model.do() + +### Create Placement of Part_4 on the top face of Part_1 +Placement_1 = model.addPlacement(partSet, [model.selection("COMPOUND", "Part_4/")], model.selection("FACE", "Part_4/Extrusion_1_1/From_Face"), model.selection("FACE", "Part_1/Extrusion_1_1/To_Face"), keepSubResults = True) + +### Create Part_5, revolution +Part_5 = model.addPart(partSet) +Part_5_doc = Part_5.document() +Sketch_4 = model.addSketch(Part_5_doc, model.defaultPlane("XOY")) +SketchCircle_3 = Sketch_4.addCircle(71.54809194299482, -51.10206832217438, 4.892982649360998) +model.do() +Revolution_1 = model.addRevolution(Part_5_doc, [model.selection("FACE", "Sketch_1/Face-SketchCircle_1_2r")], model.selection("EDGE", "PartSet/OX"), -30, 0) +model.do() + +### Create Placement_2 of Part_5 revolution to Part_3 to face +Placement_2 = model.addPlacement(partSet, [model.selection("COMPOUND", "Part_5/")], model.selection("FACE", "Part_5/Revolution_1_1/From_Face"), model.selection("FACE", "Part_3/Extrusion_1_1/To_Face"), centering = True, keepSubResults = True) +model.end() + +# Check multiple placement of Part_1, Part_3 and Part_5 after theSketch_2 +aWhatList = model.FeatureList() +aWhatList.append(Part_1.feature()) +aWhatList.append(Part_3.feature()) +aWhatList.append(Part_5.feature()) +anError = model.validateMovement(SketchCircle_2.feature(), aWhatList).strip() +assert(len(anError) == 0) +# check also that they cannot be moved after Placement_1 +anError = model.validateMovement(Placement_1.feature(), aWhatList).strip() +assert(anError == 'Placement_1 -> Part_1') +# do movement +model.do() +partSet.moveFeature(Part_1.feature(), SketchCircle_2.feature()) +partSet.moveFeature(Part_3.feature(), Part_1.feature()) +partSet.moveFeature(Part_5.feature(), Part_3.feature()) +model.end() + +# check the resulting list of features +aNames = ['Plane_4', 'Sketch_1', 'Part_1', 'Part_3', 'Part_5', 'Part_2', 'Part_4', 'Placement_1', 'Placement_2'] + +for aNameIndex in range(9): + aFeatureName = model.objectToFeature(partSet.object("Features", aNameIndex)).name() + assert(aNames[aNameIndex] == aFeatureName) diff --git a/src/ModelAPI/tests.set b/src/ModelAPI/tests.set index 5d666347f..13777dfa1 100644 --- a/src/ModelAPI/tests.set +++ b/src/ModelAPI/tests.set @@ -121,4 +121,6 @@ SET(TEST_NAMES Test19932.py Test19989.py Test20170.py + TestMovePart1.py + TestMovePart2.py ) diff --git a/src/ModuleBase/ModuleBase_Tools.cpp b/src/ModuleBase/ModuleBase_Tools.cpp index 7c51ccc9a..2245d51e0 100644 --- a/src/ModuleBase/ModuleBase_Tools.cpp +++ b/src/ModuleBase/ModuleBase_Tools.cpp @@ -480,36 +480,6 @@ void checkObjects(const QObjectPtrList& theObjects, bool& hasResult, bool& hasFe } } -/*bool setDefaultDeviationCoefficient(std::shared_ptr theGeomShape) -{ - if (!theGeomShape.get()) - return false; - // if the shape could not be exploded on faces, it contains only wires, edges, and vertices - // correction of deviation for them should not influence to the application performance - GeomAPI_ShapeExplorer anExp(theGeomShape, GeomAPI_Shape::FACE); - bool anEmpty = anExp.empty(); - return !anExp.more(); -}*/ - -/*void setDefaultDeviationCoefficient(const std::shared_ptr& theResult, - const Handle(Prs3d_Drawer)& theDrawer) -{ - if (!theResult.get()) - return; - bool aUseDeviation = false; - - std::string aResultGroup = theResult->groupName(); - if (aResultGroup == ModelAPI_ResultConstruction::group()) - aUseDeviation = true; - else if (aResultGroup == ModelAPI_ResultBody::group()) { - GeomShapePtr aGeomShape = theResult->shape(); - if (aGeomShape.get()) - aUseDeviation = setDefaultDeviationCoefficient(aGeomShape); - } - if (aUseDeviation) - theDrawer->SetDeviationCoefficient(DEFAULT_DEVIATION_COEFFICIENT); -} -*/ void setDefaultDeviationCoefficient(const TopoDS_Shape& theShape, const Handle(Prs3d_Drawer)& theDrawer) { diff --git a/src/ModuleBase/ModuleBase_WidgetLabel.cpp b/src/ModuleBase/ModuleBase_WidgetLabel.cpp index a3b445ca2..be3486c2b 100644 --- a/src/ModuleBase/ModuleBase_WidgetLabel.cpp +++ b/src/ModuleBase/ModuleBase_WidgetLabel.cpp @@ -91,10 +91,7 @@ bool ModuleBase_WidgetLabel::restoreValueCustom() std::wstring aWStr((wchar_t*)aStr); static const int aBufSize = 1000; static char aMBStr[aBufSize]; -#ifdef _DEBUG - size_t aLen = -#endif - wcstombs(aMBStr, aWStr.c_str(), aBufSize); + wcstombs(aMBStr, aWStr.c_str(), aBufSize); std::string aCodec = Config_Translator::codec(""); aText = QTextCodec::codecForName(aCodec.c_str())->toUnicode(aMBStr); } else { diff --git a/src/ModuleBase/ModuleBase_WidgetSwitch.cpp b/src/ModuleBase/ModuleBase_WidgetSwitch.cpp index 77ee2162e..7428db7d0 100644 --- a/src/ModuleBase/ModuleBase_WidgetSwitch.cpp +++ b/src/ModuleBase/ModuleBase_WidgetSwitch.cpp @@ -57,10 +57,7 @@ int ModuleBase_WidgetSwitch::addPage(ModuleBase_PageBase* thePage, const QString const QPixmap& theIcon, const QString& theTooltip) { -#ifdef _DEBUG - int aSuperCount = -#endif - ModuleBase_PagedContainer::addPage(thePage, theName, theCaseId, theIcon, theTooltip); + ModuleBase_PagedContainer::addPage(thePage, theName, theCaseId, theIcon, theTooltip); myCombo->addItem(translate(theName.toStdString())); int aResultCount = myCombo->count(); if (aResultCount == 2) diff --git a/src/PartSetPlugin/doc/PartSetPlugin.rst b/src/PartSetPlugin/doc/PartSetPlugin.rst index df635b9b2..fc5023038 100644 --- a/src/PartSetPlugin/doc/PartSetPlugin.rst +++ b/src/PartSetPlugin/doc/PartSetPlugin.rst @@ -59,3 +59,18 @@ Result Selected part is removed together with all its objects. +Move Part +----------- +The part feature may be moved in the Object Browser by mouse Drag-and-Drop. For this user should +press mouse left button on a Part feature and move it up or down along the PartSet features, then release the mouse button +to put a moved part between other PartSet features. + +The following conditions must be met: + +- PartSet document must be active. +- No operation is started. +- The moved part(s) do not break order of depended objects. + +Only enabled features could participate in the movement. If the history line arrow is located not in the end of the history +of features and the user moves the Part much lower than this arrow, the Part feature will be dropped just after the arrow +(the last active feature). diff --git a/src/XGUI/XGUI_DataModel.cpp b/src/XGUI/XGUI_DataModel.cpp index b4954c42f..e1a9ba293 100644 --- a/src/XGUI/XGUI_DataModel.cpp +++ b/src/XGUI/XGUI_DataModel.cpp @@ -19,25 +19,29 @@ #include "XGUI_DataModel.h" #include "XGUI_ObjectsBrowser.h" +#include "XGUI_Workshop.h" #include #include #include #include +#include +#include #include #include +#include +#include + #include #ifdef _MSC_VER #pragma warning(disable: 4100) #endif - - // Constructor ************************************************* XGUI_DataModel::XGUI_DataModel(QObject* theParent) : QAbstractItemModel(theParent)//, //myIsEventsProcessingBlocked(false) @@ -364,11 +368,161 @@ Qt::ItemFlags XGUI_DataModel::flags(const QModelIndex& theIndex) const { if (theIndex.isValid()) { ModuleBase_ITreeNode* aNode = (ModuleBase_ITreeNode*)theIndex.internalPointer(); - return aNode->flags(theIndex.column()); + Qt::ItemFlags aResultFlags = aNode->flags(theIndex.column()); + // Drag and drop of Part features only if: + // - PartSet is active + // - active Part feature of PartSet is dragged + // - finally if it does not break dependencies between features (but here only drag possibility is checked) + SessionPtr aSession = ModelAPI_Session::get(); + if (aSession->hasModuleDocument() && aSession->moduleDocument() == aSession->activeDocument()) { + + ObjectPtr aNodeObj = aNode->object(); + if (aNodeObj.get() && aNodeObj->groupName() == ModelAPI_Feature::group()) + { + FeaturePtr aNodeFeature = std::dynamic_pointer_cast(aNodeObj); + if (aNodeFeature.get() && aNodeFeature->getKind() == "Part" && !aNodeFeature->isDisabled()) + aResultFlags |= Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled; + } + } + return aResultFlags; } - return Qt::ItemFlags(); + return Qt::ItemIsDropEnabled | Qt::ItemFlags(); } +bool XGUI_DataModel::canDropMimeData(const QMimeData *theData, Qt::DropAction theAction, + int theRow, int theColumn, const QModelIndex &theParent) const +{ + if (theParent.isValid()) + return false; + ModuleBase_ITreeNode* aSubNode = myRoot->subNode(theRow); + if ((aSubNode && aSubNode->object() && aSubNode->object()->groupName() == ModelAPI_Feature::group()) + || theRow == myRoot->childrenCount()) // into the end of a list of features + { + return true; + } + + return false; // in other cases drop is forbidden +} + +QMimeData* XGUI_DataModel::mimeData(const QModelIndexList& theIndexes) const +{ + std::set aRows; // to avoid duplication of rows and for sorting the indices + foreach (QModelIndex anIndex, theIndexes) { + if (anIndex.isValid() && anIndex.internalPointer()) + aRows.insert(anIndex.row()); + } + QByteArray anEncodedData; + QDataStream aStream(&anEncodedData, QIODevice::WriteOnly); + for(std::set::iterator aRIter = aRows.begin(); aRIter != aRows.end(); aRIter++) + aStream << *aRIter; + + QMimeData* aMimeData = new QMimeData(); + aMimeData->setData("xgui/moved.rows", anEncodedData); + return aMimeData; +} + +bool XGUI_DataModel::dropMimeData(const QMimeData *theData, Qt::DropAction theAction, + int theRow, int theColumn, const QModelIndex &theParent) +{ + FeaturePtr aDropAfter; // after this feature it is dropped, NULL if drop the the first place + if (theRow > 0) + { + ModuleBase_ITreeNode* aNode = myRoot->subNode(theRow - 1); + if (aNode && aNode->object() && aNode->object()->groupName() == ModelAPI_Feature::group()) + aDropAfter = std::dynamic_pointer_cast(aNode->object()); + } + SessionPtr aSession = ModelAPI_Session::get(); + if (aDropAfter.get()) // move to the upper enabled feature + { + while (aDropAfter.get() && (aDropAfter->isDisabled() || !aDropAfter->isInHistory())) + aDropAfter = aDropAfter->document()->nextFeature(aDropAfter, true); + } + else { // move after invisible items, not the first (which is coordinate system by default) + std::list allFeatures = aSession->get()->moduleDocument()->allFeatures(); + std::list::iterator aFeature = allFeatures.begin(); + for(; aFeature != allFeatures.end(); aFeature++) + { + if ((*aFeature)->isInHistory()) + break; + aDropAfter = *aFeature; + } + } + // move after the composite feature memebers, if they are invisible (sub elements of sketch) + CompositeFeaturePtr aComposite = std::dynamic_pointer_cast(aDropAfter); + if (aComposite.get()) + { + FeaturePtr aNext = aDropAfter->document()->nextFeature(aDropAfter); + while (aNext.get() && !aNext->isInHistory() && aComposite->isSub(aNext)) { + aDropAfter = aNext; + aNext = aDropAfter->document()->nextFeature(aNext); + } + } + + QByteArray anEncodedData = theData->data("xgui/moved.rows"); + if (anEncodedData.isEmpty()) + return false; // dropped something alien, decline + + QDataStream stream(&anEncodedData, QIODevice::ReadOnly); + std::list aDropped; + while (!stream.atEnd()) { + int aRow; + stream >> aRow; + ModuleBase_ITreeNode* aNode = myRoot->subNode(aRow); + if (aNode) + { + FeaturePtr aFeature = std::dynamic_pointer_cast(aNode->object()); + // feature moved after itself is not moved, add only Part feature, other skip + if (aFeature.get() && aFeature != aDropAfter && aFeature->getKind() == "Part") + aDropped.push_back(aFeature); + } + } + if (aDropped.empty()) // nothing to move + return false; + + // check for the movement is valid due to existing dependencies + std::wstring anErrorStr = ModelAPI_Tools::validateMovement(aDropAfter, aDropped); + if (!anErrorStr.empty()) + { + QMessageBox aMessageBox; + aMessageBox.setWindowTitle(QObject::tr("Move part")); + aMessageBox.setIcon(QMessageBox::Warning); + aMessageBox.setStandardButtons(QMessageBox::Ok); + aMessageBox.setDefaultButton(QMessageBox::Ok); + QString aMessageText(QObject::tr("Part(s) cannot be moved because of breaking dependencies.")); + aMessageBox.setText(aMessageText); + aMessageBox.setDetailedText(QString::fromStdWString(anErrorStr)); + aMessageBox.exec(); + return false; + } + + if (aSession->isOperation()) + { + QMessageBox aMessageBox; + aMessageBox.setWindowTitle(QObject::tr("Move part")); + aMessageBox.setIcon(QMessageBox::Warning); + aMessageBox.setStandardButtons(QMessageBox::Ok); + aMessageBox.setDefaultButton(QMessageBox::Ok); + QString aMessageText(QObject::tr("Cannot move part(s) during another operation.")); + aMessageBox.setText(aMessageText); + aMessageBox.exec(); + return false; + } + + aSession->startOperation("Move Part"); + DocumentPtr aPartSet = aSession->moduleDocument(); + for (std::list::iterator aDrop = aDropped.begin(); aDrop != aDropped.end(); aDrop++) + { + aPartSet->moveFeature(*aDrop, aDropAfter); + aDropAfter = *aDrop; + } + aSession->finishOperation(); + + updateSubTree(myRoot); + myWorkshop->updateHistory(); + + // returns false in any case to avoid calling removeRows after it, + return false; // because number of rows stays the same +} //****************************************************** QModelIndex XGUI_DataModel::documentRootIndex(DocumentPtr theDoc, int theColumn) const diff --git a/src/XGUI/XGUI_DataModel.h b/src/XGUI/XGUI_DataModel.h index 6a9a9b6e3..d8dca75b5 100644 --- a/src/XGUI/XGUI_DataModel.h +++ b/src/XGUI/XGUI_DataModel.h @@ -132,18 +132,20 @@ public: /// \param theIndex a model index virtual Qt::ItemFlags flags(const QModelIndex& theIndex) const; - /// Returns an index which is root of the given document - /// \param theDoc a document - QModelIndex documentRootIndex(DocumentPtr theDoc, int theColumn = 1) const; + /// Returns true if a model can accept a drop of the data (used for drag and drop functionality). + virtual bool canDropMimeData(const QMimeData *theData, Qt::DropAction theAction, + int theRow, int theColumn, const QModelIndex &theParent) const override; - /// Returns last history object index - //virtual QModelIndex lastHistoryIndex() const; + /// Converts the dragged items information into mime data format (to be encoded in dropMimeData) + virtual QMimeData* mimeData(const QModelIndexList &indexes) const override; - /// Initialises XML data model reader. It must be initialised before DataModel using. - //void setXMLReader(Config_DataModelReader* theReader) { myXMLReader = theReader; } + /// Performs a drag and drop of Part feature operation when it is droped. + virtual bool dropMimeData(const QMimeData *theData, Qt::DropAction theAction, + int theRow, int theColumn, const QModelIndex &theParent) override; - /// Do not processing anymore events of model loop - //bool blockEventsProcessing(const bool theState); + /// Returns an index which is root of the given document + /// \param theDoc a document + QModelIndex documentRootIndex(DocumentPtr theDoc, int theColumn = 1) const; /// Returns true if the data model item has Hidden visual state /// \param theIndex a tree model item @@ -167,6 +169,12 @@ public: /// \param thCol a column QModelIndex getIndex(ModuleBase_ITreeNode* theNode, int thCol) const; + /// Allows to support drag and drop of some model items + virtual Qt::DropActions supportedDropActions() const override + { + return Qt::MoveAction; + } + signals: /// Signal send before tree rebuild void beforeTreeRebuild(); @@ -186,69 +194,8 @@ private: void updateSubTree(ModuleBase_ITreeNode* theParent); - /// Find a root index which contains objects of the given document - /// \param theDoc the document object - //QModelIndex findDocumentRootIndex(const ModelAPI_Document* theDoc, int aColumn = 1) const; - - /// Returns number of folders in document. - /// Considered folders which has to be shown only if they are not empty. - /// \param theDoc document which has to be checked. If 0 then Root document will be considered - //int foldersCount(ModelAPI_Document* theDoc = 0) const; - - /// Retrurns indexes of folders which can not be shown because they are empty - /// \param theDoc document which has to be checked. If 0 then Root document will be considered - //QIntList missedFolderIndexes(ModelAPI_Document* theDoc = 0) const; - - /// Returns Id (row) of a folder taking into consideration - /// folders which can not be shown non empty - /// \param theType Type of the folder - /// \param theDoc a document which contains this folder - //int folderId(std::string theType, ModelAPI_Document* theDoc = 0) const; - - /// Removes a row from branch of tree - /// \param theStart - start row to update indexes - /// \param theSize - number of indexes in the folder - /// \param theParent - index of parent folder - //void rebuildBranch(int theRow, int theCount, const QModelIndex& theParent = QModelIndex()); - - /// Returns list of folders types which can not be shown empty - /// \param fromRoot - root document flag - //QStringList listOfShowNotEmptyFolders(bool fromRoot = true) const; - - //int getNumberOfFolderItems(const ModelAPI_Folder* theFolder) const; - //ObjectPtr getObjectInFolder(const ModelAPI_Folder* theFolder, int theId) const; - - //VisibilityState getVisibilityState(const QModelIndex& theIndex) const; - - //void addShownFolder(DocumentPtr theDoc, QString theFolder) - //{ - // if (!myShownFolders.contains(theDoc)) { - // myShownFolders[theDoc] = QStringList(); - // } - // myShownFolders[theDoc].append(theFolder); - //} - - //void removeShownFolder(DocumentPtr theDoc, QString theFolder) - //{ - // if (myShownFolders.contains(theDoc)) { - // myShownFolders[theDoc].removeAll(theFolder); - // if (myShownFolders[theDoc].isEmpty()) - // myShownFolders.remove(theDoc); - // } - //} - - //bool hasShownFolder(DocumentPtr theDoc, QString theFolder) const - //{ - // if (myShownFolders.contains(theDoc)) - // return myShownFolders[theDoc].contains(theFolder); - // return false; - //} - - //Config_DataModelReader* myXMLReader; - XGUI_Workshop* myWorkshop; QMap myShownFolders; - //bool myIsEventsProcessingBlocked; ModuleBase_ITreeNode* myRoot; }; diff --git a/src/XGUI/XGUI_MenuMgr.cpp b/src/XGUI/XGUI_MenuMgr.cpp index 4339bf6bc..6aa634fe3 100644 --- a/src/XGUI/XGUI_MenuMgr.cpp +++ b/src/XGUI/XGUI_MenuMgr.cpp @@ -206,12 +206,12 @@ QAction* XGUI_MenuMgr::buildAction(const std::shared_ptr& XGUI_OperationMgr* anOperationMgr = myWorkshop->operationMgr(); XGUI_ActionsMgr* anActionsMgr = myWorkshop->actionsMgr(); if (aNestedActions.contains(FEATURE_WHEN_NESTED_ACCEPT)) { - QAction* anAction = anActionsMgr->operationStateAction(XGUI_ActionsMgr::AcceptAll); + anAction = anActionsMgr->operationStateAction(XGUI_ActionsMgr::AcceptAll); QObject::connect(anAction, SIGNAL(triggered()), anOperationMgr, SLOT(commitAllOperations())); aNestedActList << anAction; } if (aNestedActions.contains(FEATURE_WHEN_NESTED_ABORT)) { - QAction* anAction = anActionsMgr->operationStateAction(XGUI_ActionsMgr::AbortAll); + anAction = anActionsMgr->operationStateAction(XGUI_ActionsMgr::AbortAll); QObject::connect(anAction, SIGNAL(triggered()), anOperationMgr, SLOT(onAbortAllOperation())); aNestedActList << anAction; } diff --git a/src/XGUI/XGUI_ObjectsBrowser.cpp b/src/XGUI/XGUI_ObjectsBrowser.cpp index 34c113f2c..49ce56a3d 100644 --- a/src/XGUI/XGUI_ObjectsBrowser.cpp +++ b/src/XGUI/XGUI_ObjectsBrowser.cpp @@ -94,6 +94,13 @@ XGUI_DataTree::XGUI_DataTree(QWidget* theParent) setSelectionBehavior(QAbstractItemView::SelectRows); setSelectionMode(QAbstractItemView::ExtendedSelection); + // drag and drop + setDragEnabled(true); + setAcceptDrops(true); + viewport()->setAcceptDrops(true); + setDropIndicatorShown(true); + setDragDropMode(QAbstractItemView::InternalMove); + setItemDelegateForColumn(1, new XGUI_TreeViewItemDelegate(this)); connect(this, SIGNAL(doubleClicked(const QModelIndex&)), diff --git a/src/XGUI/XGUI_msg_fr.ts b/src/XGUI/XGUI_msg_fr.ts index 2e320d15e..4f5f1ee3b 100644 --- a/src/XGUI/XGUI_msg_fr.ts +++ b/src/XGUI/XGUI_msg_fr.ts @@ -47,6 +47,18 @@ Part files (*.shaperpart);;All files (*.*) Fichiers pièces (*.shaperpart);;Tous les fichiers (*.*) + + Part(s) cannot be moved because of breaking dependencies. + La ou les pièces ne peuvent pas être déplacées en raison de la rupture des dépendances. + + + Move part + Déplacer une partie + + + Cannot move part(s) during another operation. + Impossible de déplacer la pièce lors d'une autre opération. + XGUI_ActionsMgr -- 2.39.2