diff --git a/.github/workflows/CI_master.yml b/.github/workflows/CI_master.yml index ca74ea454c..55b2db64d7 100644 --- a/.github/workflows/CI_master.yml +++ b/.github/workflows/CI_master.yml @@ -26,7 +26,7 @@ name: FreeCAD master CI -on: [workflow_dispatch, push, pull_request] +on: [workflow_dispatch, push, pull_request, merge_group] concurrency: group: FC-CI-${{ github.head_ref || github.run_id }} diff --git a/.github/workflows/auto-close_stale_issues_and_pull-requests.yml b/.github/workflows/auto-close_stale_issues_and_pull-requests.yml index ffcafbbd90..9433dbe202 100644 --- a/.github/workflows/auto-close_stale_issues_and_pull-requests.yml +++ b/.github/workflows/auto-close_stale_issues_and_pull-requests.yml @@ -22,7 +22,7 @@ jobs: steps: - name: '🧹 Tag & close stale unconfirmed bugs' id: stale_issues - uses: actions/stale@v9.0.0 + uses: actions/stale@v9.1.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: -1 @@ -49,7 +49,7 @@ jobs: - name: '🧹 Close stale requested feedback issues' id: awaiting_issues - uses: actions/stale@v9.0.0 + uses: actions/stale@v9.1.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: -1 @@ -77,7 +77,7 @@ jobs: - name: '🧹 Tag & close inactive issues' id: inactive_issues - uses: actions/stale@v9.0.0 + uses: actions/stale@v9.1.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: -1 @@ -108,7 +108,7 @@ jobs: - name: '🧹 Tag & close inactive PRs' id: inactive_pr - uses: actions/stale@v9.0.0 + uses: actions/stale@v9.1.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: -1 diff --git a/.github/workflows/sub_lint.yml b/.github/workflows/sub_lint.yml index ca829e3dbc..92b38b2199 100644 --- a/.github/workflows/sub_lint.yml +++ b/.github/workflows/sub_lint.yml @@ -276,103 +276,32 @@ jobs: if: inputs.checkLineendings && always() continue-on-error: ${{ inputs.lineendingsFailSilent }} run: | - lineendings=0 - for file in ${{ inputs.changedFiles }} - do - # Check for DOS or MAC line endings - if [[ $(file -b $file) =~ "with CR" ]] - then - echo "::warning file=$file,title=$file::File has non Unix line endings" - echo "$file has non Unix line endings" >> ${{ env.logdir }}lineendings.log - ((lineendings=lineendings+1)) - fi - done - echo "Found $lineendings line ending errors" - # Write the report - if [ $lineendings -gt 0 ] - then - echo "
:information_source: Found $lineendings problems with line endings" >> ${{env.reportdir}}${{ env.reportfilename }} - echo "" >> ${{env.reportdir}}${{ env.reportfilename }} - echo '```' >> ${{env.reportdir}}${{ env.reportfilename }} - cat ${{ env.logdir }}lineendings.log >> ${{env.reportdir}}${{ env.reportfilename }} - echo '```' >> ${{env.reportdir}}${{ env.reportfilename }} - echo "
" >> ${{env.reportdir}}${{ env.reportfilename }} - else - echo ":heavy_check_mark: No line ending problem found " >> ${{env.reportdir}}${{ env.reportfilename }} - fi - echo "" >> ${{env.reportdir}}${{ env.reportfilename }} - # Exit the step with appropriate code - [ $lineendings -eq 0 ] + python3 tools/lint/generic_checks.py \ + --lineendings-check \ + --files "${{ inputs.changedFiles }}" \ + --report-file "${{ env.reportdir }}${{ env.reportfilename }}" \ + --log-dir "${{ env.logdir }}" + - name: Check for trailing whitespaces if: inputs.checkWhitespace && always() continue-on-error: ${{ inputs.whitespaceFailSilent }} run: | - whitespaceErrors=0 - exclude="*[.md]" - for file in ${{ inputs.changedFiles }} - do - # Check for trailing whitespaces - grep -nIHE --exclude="$exclude" " $" $file | sed -e "s/$/<-- trailing whitespace/" >> ${{ env.logdir }}whitespace.log || true - done - # Write the Log to the console with the Problem Matchers - if [ -f ${{ env.logdir }}whitespace.log ] - then - echo "::add-matcher::${{ runner.workspace }}/FreeCAD/.github/problemMatcher/grepMatcherWarning.json" - cat ${{ env.logdir }}whitespace.log - echo "::remove-matcher owner=grepMatcher-warning::" - whitespaceErrors=$(wc -l < ${{ env.logdir }}whitespace.log) - fi - echo "Found $whitespaceErrors whitespace errors" - # Write the report - if [ $whitespaceErrors -gt 0 ] - then - echo "
:information_source: Found $whitespaceErrors trailing whitespace" >> ${{env.reportdir}}${{ env.reportfilename }} - echo "" >> ${{env.reportdir}}${{ env.reportfilename }} - echo '```' >> ${{env.reportdir}}${{ env.reportfilename }} - cat ${{ env.logdir }}whitespace.log >> ${{env.reportdir}}${{ env.reportfilename }} - echo '```' >> ${{env.reportdir}}${{ env.reportfilename }} - echo "
" >> ${{env.reportdir}}${{ env.reportfilename }} - else - echo ":heavy_check_mark: No trailing whitespace found " >> ${{env.reportdir}}${{ env.reportfilename }} - fi - echo "" >> ${{env.reportdir}}${{ env.reportfilename }} - # Exit the step with appropriate code - [ $whitespaceErrors -eq 0 ] + python3 tools/lint/generic_checks.py \ + --whitespace-check \ + --files "${{ inputs.changedFiles }}" \ + --report-file "${{ env.reportdir }}${{ env.reportfilename }}" \ + --log-dir "${{ env.logdir }}" + - name: Check for Tab usage if: inputs.checkTabs && always() continue-on-error: ${{ inputs.tabsFailSilent }} run: | - tabErrors=0 - exclude="*[.md]" - # Check for Tab usage - for file in ${{ steps.changed-files.outputs.all_changed_files }} - do - grep -nIHE --exclude="$exclude" $'\t' $file | sed -e "s/$/ <-- contains tab/" >> ${{ env.logdir }}tab.log || true - done - # Write the Log to the console with the Problem Matchers - if [ -f ${{ env.logdir }}tab.log ] - then - echo "::add-matcher::${{ runner.workspace }}/FreeCAD/.github/problemMatcher/grepMatcherWarning.json" - cat ${{ env.logdir }}tab.log - echo "::remove-matcher owner=grepMatcher-warning::" - tabErrors=$(wc -l < ${{ env.logdir }}tab.log) - fi - echo "Found $tabErrors tab errors" - # Write the report - if [ $tabErrors -gt 0 ]; then - echo "
:information_source: Found $tabErrors tabs, better to use spaces" >> ${{env.reportdir}}${{ env.reportfilename }} - echo "" >> ${{env.reportdir}}${{ env.reportfilename }} - echo '```' >> ${{env.reportdir}}${{ env.reportfilename }} - cat ${{ env.logdir }}tab.log >> ${{env.reportdir}}${{ env.reportfilename }} - echo '```' >> ${{env.reportdir}}${{ env.reportfilename }} - echo "
" >> ${{env.reportdir}}${{ env.reportfilename }} - else - echo ":heavy_check_mark: No tabs found" >> ${{env.reportdir}}${{ env.reportfilename }} - fi - echo "" >> ${{env.reportdir}}${{ env.reportfilename }} - # Exit the step with appropriate code - [ $tabErrors -eq 0 ] - # Run Python lints + python3 tools/lint/generic_checks.py \ + --tabs-check \ + --files "${{ inputs.changedFiles }}" \ + --report-file "${{ env.reportdir }}${{ env.reportfilename }}" \ + --log-dir "${{ env.logdir }}" + - name: Pylint if: inputs.checkPylint && inputs.changedPythonFiles != '' && always() continue-on-error: ${{ inputs.pylintFailSilent }} diff --git a/cMake/FindOCC.cmake b/cMake/FindOCC.cmake index 21e9465282..552f0b1896 100644 --- a/cMake/FindOCC.cmake +++ b/cMake/FindOCC.cmake @@ -10,12 +10,10 @@ # we first try to find opencascade directly: if (NOT OCCT_CMAKE_FALLBACK) find_package(OpenCASCADE CONFIG QUIET) - if (NOT (CMAKE_VERSION VERSION_LESS 3.6.0)) - get_property(flags DIRECTORY PROPERTY COMPILE_DEFINITIONS) - # OCCT 7.5 adds this define that causes hundreds of compiler warnings with Qt5.x, so remove it again - list(FILTER flags EXCLUDE REGEX [[GL_GLEXT_LEGACY]]) - set_property(DIRECTORY PROPERTY COMPILE_DEFINITIONS ${flags}) - endif () + get_property(flags DIRECTORY PROPERTY COMPILE_DEFINITIONS) + # OCCT 7.5 adds this define that causes hundreds of compiler warnings with Qt5.x, so remove it again + list(FILTER flags EXCLUDE REGEX [[GL_GLEXT_LEGACY]]) + set_property(DIRECTORY PROPERTY COMPILE_DEFINITIONS ${flags}) endif () if (OpenCASCADE_FOUND) set(OCC_FOUND ${OpenCASCADE_FOUND}) diff --git a/cMake/FreeCAD_Helpers/InitializeFreeCADBuildOptions.cmake b/cMake/FreeCAD_Helpers/InitializeFreeCADBuildOptions.cmake index 6b19043f77..6e8c8e42b0 100644 --- a/cMake/FreeCAD_Helpers/InitializeFreeCADBuildOptions.cmake +++ b/cMake/FreeCAD_Helpers/InitializeFreeCADBuildOptions.cmake @@ -13,6 +13,8 @@ macro(InitializeFreeCADBuildOptions) option(FREECAD_USE_FREETYPE "Builds the features using FreeType libs" ON) option(FREECAD_BUILD_DEBIAN "Prepare for a build of a Debian package" OFF) option(FREECAD_CHECK_PIVY "Check for pivy version using Python at build time" ON) + option(FREECAD_PARALLEL_COMPILE_JOBS "Compilation jobs pool size to fit memory limitations.") + option(FREECAD_PARALLEL_LINK_JOBS "Linkage jobs pool size to fit memory limitations.") option(BUILD_WITH_CONDA "Set ON if you build FreeCAD with conda" OFF) option(BUILD_DYNAMIC_LINK_PYTHON "If OFF extension-modules do not link against python-libraries" ON) option(INSTALL_TO_SITEPACKAGES "If ON the freecad root namespace (python) is installed into python's site-packages" ON) @@ -206,4 +208,22 @@ macro(InitializeFreeCADBuildOptions) "Please choose another build directory! Or disable the option BUILD_FORCE_DIRECTORY.") endif() endif() + + if(FREECAD_PARALLEL_COMPILE_JOBS) + if(CMAKE_GENERATOR MATCHES "Ninja") + set_property(GLOBAL APPEND PROPERTY JOB_POOLS compile_job_pool=${FREECAD_PARALLEL_COMPILE_JOBS}) + set(CMAKE_JOB_POOL_COMPILE compile_job_pool) + else() + message(WARNING "Job pooling is only available with Ninja generators.") + endif() + endif() + + if(FREECAD_PARALLEL_LINK_JOBS) + if(CMAKE_GENERATOR MATCHES "Ninja") + set_property(GLOBAL APPEND PROPERTY JOB_POOLS link_job_pool=${FREECAD_PARALLEL_LINK_JOBS}) + set(CMAKE_JOB_POOL_LINK link_job_pool) + else() + message(WARNING "Job pooling is only available with Ninja generators.") + endif() + endif() endmacro(InitializeFreeCADBuildOptions) diff --git a/src/App/Application.cpp b/src/App/Application.cpp index 507b063f51..c149e20df4 100644 --- a/src/App/Application.cpp +++ b/src/App/Application.cpp @@ -302,12 +302,12 @@ void Application::setupPythonTypes() // NOTE: To finish the initialization of our own type objects we must // call PyType_Ready, otherwise we run into a segmentation fault, later on. // This function is responsible for adding inherited slots from a type's base class. - Base::Interpreter().addType(&Base::VectorPy::Type, pAppModule, "Vector"); - Base::Interpreter().addType(&Base::MatrixPy::Type, pAppModule, "Matrix"); - Base::Interpreter().addType(&Base::BoundBoxPy::Type, pAppModule, "BoundBox"); - Base::Interpreter().addType(&Base::PlacementPy::Type, pAppModule, "Placement"); - Base::Interpreter().addType(&Base::RotationPy::Type, pAppModule, "Rotation"); - Base::Interpreter().addType(&Base::AxisPy::Type, pAppModule, "Axis"); + Base::InterpreterSingleton::addType(&Base::VectorPy::Type, pAppModule, "Vector"); + Base::InterpreterSingleton::addType(&Base::MatrixPy::Type, pAppModule, "Matrix"); + Base::InterpreterSingleton::addType(&Base::BoundBoxPy::Type, pAppModule, "BoundBox"); + Base::InterpreterSingleton::addType(&Base::PlacementPy::Type, pAppModule, "Placement"); + Base::InterpreterSingleton::addType(&Base::RotationPy::Type, pAppModule, "Rotation"); + Base::InterpreterSingleton::addType(&Base::AxisPy::Type, pAppModule, "Axis"); // Note: Create an own module 'Base' which should provide the python // binding classes from the base module. At a later stage we should @@ -324,39 +324,39 @@ void Application::setupPythonTypes() // Python types - Base::Interpreter().addType(&Base::VectorPy ::Type,pBaseModule,"Vector"); - Base::Interpreter().addType(&Base::MatrixPy ::Type,pBaseModule,"Matrix"); - Base::Interpreter().addType(&Base::BoundBoxPy ::Type,pBaseModule,"BoundBox"); - Base::Interpreter().addType(&Base::PlacementPy ::Type,pBaseModule,"Placement"); - Base::Interpreter().addType(&Base::RotationPy ::Type,pBaseModule,"Rotation"); - Base::Interpreter().addType(&Base::AxisPy ::Type,pBaseModule,"Axis"); - Base::Interpreter().addType(&Base::CoordinateSystemPy::Type,pBaseModule,"CoordinateSystem"); - Base::Interpreter().addType(&Base::TypePy ::Type,pBaseModule,"TypeId"); - Base::Interpreter().addType(&Base::PrecisionPy ::Type,pBaseModule,"Precision"); + Base::InterpreterSingleton::addType(&Base::VectorPy ::Type,pBaseModule,"Vector"); + Base::InterpreterSingleton::addType(&Base::MatrixPy ::Type,pBaseModule,"Matrix"); + Base::InterpreterSingleton::addType(&Base::BoundBoxPy ::Type,pBaseModule,"BoundBox"); + Base::InterpreterSingleton::addType(&Base::PlacementPy ::Type,pBaseModule,"Placement"); + Base::InterpreterSingleton::addType(&Base::RotationPy ::Type,pBaseModule,"Rotation"); + Base::InterpreterSingleton::addType(&Base::AxisPy ::Type,pBaseModule,"Axis"); + Base::InterpreterSingleton::addType(&Base::CoordinateSystemPy::Type,pBaseModule,"CoordinateSystem"); + Base::InterpreterSingleton::addType(&Base::TypePy ::Type,pBaseModule,"TypeId"); + Base::InterpreterSingleton::addType(&Base::PrecisionPy ::Type,pBaseModule,"Precision"); - Base::Interpreter().addType(&App::MaterialPy::Type, pAppModule, "Material"); - Base::Interpreter().addType(&App::MetadataPy::Type, pAppModule, "Metadata"); + Base::InterpreterSingleton::addType(&MaterialPy::Type, pAppModule, "Material"); + Base::InterpreterSingleton::addType(&MetadataPy::Type, pAppModule, "Metadata"); - Base::Interpreter().addType(&App::MeasureManagerPy::Type, pAppModule, "MeasureManager"); + Base::InterpreterSingleton::addType(&MeasureManagerPy::Type, pAppModule, "MeasureManager"); - Base::Interpreter().addType(&App::StringHasherPy::Type, pAppModule, "StringHasher"); - Base::Interpreter().addType(&App::StringIDPy::Type, pAppModule, "StringID"); + Base::InterpreterSingleton::addType(&StringHasherPy::Type, pAppModule, "StringHasher"); + Base::InterpreterSingleton::addType(&StringIDPy::Type, pAppModule, "StringID"); // Add document types - Base::Interpreter().addType(&App::PropertyContainerPy::Type, pAppModule, "PropertyContainer"); - Base::Interpreter().addType(&App::ExtensionContainerPy::Type, pAppModule, "ExtensionContainer"); - Base::Interpreter().addType(&App::DocumentPy::Type, pAppModule, "Document"); - Base::Interpreter().addType(&App::DocumentObjectPy::Type, pAppModule, "DocumentObject"); - Base::Interpreter().addType(&App::DocumentObjectGroupPy::Type, pAppModule, "DocumentObjectGroup"); - Base::Interpreter().addType(&App::GeoFeaturePy::Type, pAppModule, "GeoFeature"); + Base::InterpreterSingleton::addType(&PropertyContainerPy::Type, pAppModule, "PropertyContainer"); + Base::InterpreterSingleton::addType(&ExtensionContainerPy::Type, pAppModule, "ExtensionContainer"); + Base::InterpreterSingleton::addType(&DocumentPy::Type, pAppModule, "Document"); + Base::InterpreterSingleton::addType(&DocumentObjectPy::Type, pAppModule, "DocumentObject"); + Base::InterpreterSingleton::addType(&DocumentObjectGroupPy::Type, pAppModule, "DocumentObjectGroup"); + Base::InterpreterSingleton::addType(&GeoFeaturePy::Type, pAppModule, "GeoFeature"); // Add extension types - Base::Interpreter().addType(&App::ExtensionPy::Type, pAppModule, "Extension"); - Base::Interpreter().addType(&App::DocumentObjectExtensionPy::Type, pAppModule, "DocumentObjectExtension"); - Base::Interpreter().addType(&App::GroupExtensionPy::Type, pAppModule, "GroupExtension"); - Base::Interpreter().addType(&App::GeoFeatureGroupExtensionPy::Type, pAppModule, "GeoFeatureGroupExtension"); - Base::Interpreter().addType(&App::OriginGroupExtensionPy::Type, pAppModule, "OriginGroupExtension"); - Base::Interpreter().addType(&App::LinkBaseExtensionPy::Type, pAppModule, "LinkBaseExtension"); + Base::InterpreterSingleton::addType(&ExtensionPy::Type, pAppModule, "Extension"); + Base::InterpreterSingleton::addType(&DocumentObjectExtensionPy::Type, pAppModule, "DocumentObjectExtension"); + Base::InterpreterSingleton::addType(&GroupExtensionPy::Type, pAppModule, "GroupExtension"); + Base::InterpreterSingleton::addType(&GeoFeatureGroupExtensionPy::Type, pAppModule, "GeoFeatureGroupExtension"); + Base::InterpreterSingleton::addType(&OriginGroupExtensionPy::Type, pAppModule, "OriginGroupExtension"); + Base::InterpreterSingleton::addType(&LinkBaseExtensionPy::Type, pAppModule, "LinkBaseExtension"); //insert Base and Console Py_INCREF(pBaseModule); @@ -377,81 +377,49 @@ void Application::setupPythonTypes() nullptr, nullptr, nullptr, nullptr }; PyObject* pUnitsModule = PyModule_Create(&UnitsModuleDef); - Base::Interpreter().addType(&Base::QuantityPy ::Type,pUnitsModule,"Quantity"); + Base::InterpreterSingleton::addType(&Base::QuantityPy ::Type,pUnitsModule,"Quantity"); // make sure to set the 'nb_true_divide' slot - Base::Interpreter().addType(&Base::UnitPy ::Type,pUnitsModule,"Unit"); + Base::InterpreterSingleton::addType(&Base::UnitPy ::Type,pUnitsModule,"Unit"); Py_INCREF(pUnitsModule); PyModule_AddObject(pAppModule, "Units", pUnitsModule); Base::ProgressIndicatorPy::init_type(); - Base::Interpreter().addType(Base::ProgressIndicatorPy::type_object(), + Base::InterpreterSingleton::addType(Base::ProgressIndicatorPy::type_object(), pBaseModule,"ProgressIndicator"); Base::Vector2dPy::init_type(); - Base::Interpreter().addType(Base::Vector2dPy::type_object(), + Base::InterpreterSingleton::addType(Base::Vector2dPy::type_object(), pBaseModule,"Vector2d"); // clang-format on } -// clang-format off +/* + * Define custom Python exception types + */ void Application::setupPythonException(PyObject* module) { - // Define custom Python exception types - // - Base::PyExc_FC_GeneralError = PyErr_NewException("Base.FreeCADError", PyExc_RuntimeError, nullptr); - Py_INCREF(Base::PyExc_FC_GeneralError); - PyModule_AddObject(module, "FreeCADError", Base::PyExc_FC_GeneralError); + auto setup = [&module, str {"Base."}](const std::string& ename, auto pyExcType) { + auto exception = PyErr_NewException((str + ename).c_str(), pyExcType, nullptr); + Py_INCREF(exception); + PyModule_AddObject(module, ename.c_str(), exception); + return exception; + }; - Base::PyExc_FC_FreeCADAbort = PyErr_NewException("Base.FreeCADAbort", PyExc_BaseException, nullptr); - Py_INCREF(Base::PyExc_FC_FreeCADAbort); - PyModule_AddObject(module, "FreeCADAbort", Base::PyExc_FC_FreeCADAbort); - - Base::PyExc_FC_XMLBaseException = PyErr_NewException("Base.XMLBaseException", PyExc_Exception, nullptr); - Py_INCREF(Base::PyExc_FC_XMLBaseException); - PyModule_AddObject(module, "XMLBaseException", Base::PyExc_FC_XMLBaseException); - - Base::PyExc_FC_XMLParseException = PyErr_NewException("Base.XMLParseException", Base::PyExc_FC_XMLBaseException, nullptr); - Py_INCREF(Base::PyExc_FC_XMLParseException); - PyModule_AddObject(module, "XMLParseException", Base::PyExc_FC_XMLParseException); - - Base::PyExc_FC_XMLAttributeError = PyErr_NewException("Base.XMLAttributeError", Base::PyExc_FC_XMLBaseException, nullptr); - Py_INCREF(Base::PyExc_FC_XMLAttributeError); - PyModule_AddObject(module, "XMLAttributeError", Base::PyExc_FC_XMLAttributeError); - - Base::PyExc_FC_UnknownProgramOption = PyErr_NewException("Base.UnknownProgramOption", PyExc_BaseException, nullptr); - Py_INCREF(Base::PyExc_FC_UnknownProgramOption); - PyModule_AddObject(module, "UnknownProgramOption", Base::PyExc_FC_UnknownProgramOption); - - Base::PyExc_FC_BadFormatError = PyErr_NewException("Base.BadFormatError", Base::PyExc_FC_GeneralError, nullptr); - Py_INCREF(Base::PyExc_FC_BadFormatError); - PyModule_AddObject(module, "BadFormatError", Base::PyExc_FC_BadFormatError); - - Base::PyExc_FC_BadGraphError = PyErr_NewException("Base.BadGraphError", Base::PyExc_FC_GeneralError, nullptr); - Py_INCREF(Base::PyExc_FC_BadGraphError); - PyModule_AddObject(module, "BadGraphError", Base::PyExc_FC_BadGraphError); - - Base::PyExc_FC_ExpressionError = PyErr_NewException("Base.ExpressionError", Base::PyExc_FC_GeneralError, nullptr); - Py_INCREF(Base::PyExc_FC_ExpressionError); - PyModule_AddObject(module, "ExpressionError", Base::PyExc_FC_ExpressionError); - - Base::PyExc_FC_ParserError = PyErr_NewException("Base.ParserError", Base::PyExc_FC_GeneralError, nullptr); - Py_INCREF(Base::PyExc_FC_ParserError); - PyModule_AddObject(module, "ParserError", Base::PyExc_FC_ParserError); - - Base::PyExc_FC_CADKernelError = PyErr_NewException("Base.CADKernelError", Base::PyExc_FC_GeneralError, nullptr); - Py_INCREF(Base::PyExc_FC_CADKernelError); - PyModule_AddObject(module, "CADKernelError", Base::PyExc_FC_CADKernelError); - - Base::PyExc_FC_PropertyError = PyErr_NewException("Base.PropertyError", PyExc_AttributeError, nullptr); - Py_INCREF(Base::PyExc_FC_PropertyError); - PyModule_AddObject(module, "PropertyError", Base::PyExc_FC_PropertyError); - - Base::PyExc_FC_AbortIOException = PyErr_NewException("Base.PyExc_FC_AbortIOException", PyExc_BaseException, nullptr); - Py_INCREF(Base::PyExc_FC_AbortIOException); - PyModule_AddObject(module, "AbortIOException", Base::PyExc_FC_AbortIOException); + PyExc_FC_GeneralError = setup("FreeCADError", PyExc_RuntimeError); + PyExc_FC_FreeCADAbort = setup("FreeCADAbort", PyExc_BaseException); + PyExc_FC_XMLBaseException = setup("XMLBaseException", PyExc_Exception); + PyExc_FC_XMLParseException = setup("XMLParseException", PyExc_FC_XMLBaseException); + PyExc_FC_XMLAttributeError = setup("XMLAttributeError", PyExc_FC_XMLBaseException); + PyExc_FC_UnknownProgramOption = setup("UnknownProgramOption", PyExc_BaseException); + PyExc_FC_BadFormatError = setup("BadFormatError", PyExc_FC_GeneralError); + PyExc_FC_BadGraphError = setup("BadGraphError", PyExc_FC_GeneralError); + PyExc_FC_ExpressionError = setup("ExpressionError", PyExc_FC_GeneralError); + PyExc_FC_ParserError = setup("ParserError", PyExc_FC_GeneralError); + PyExc_FC_CADKernelError = setup("CADKernelError", PyExc_FC_GeneralError); + PyExc_FC_PropertyError = setup("PropertyError", PyExc_AttributeError); + PyExc_FC_AbortIOException = setup("AbortIOException", PyExc_BaseException); } -// clang-format on //************************************************************************** // Interface @@ -551,7 +519,7 @@ Document* Application::newDocument(const char * proposedName, const char * propo bool Application::closeDocument(const char* name) { - map::iterator pos = DocMap.find( name ); + auto pos = DocMap.find( name ); if (pos == DocMap.end()) // no such document return false; @@ -586,9 +554,8 @@ void Application::closeAllDocuments() App::Document* Application::getDocument(const char *Name) const { - std::map::const_iterator pos; - pos = DocMap.find(Name); + const auto pos = DocMap.find(Name); if (pos == DocMap.end()) return nullptr; @@ -610,6 +577,7 @@ const char * Application::getDocumentName(const App::Document* doc) const std::vector Application::getDocuments() const { std::vector docs; + docs.reserve(DocMap.size()); for (const auto & it : DocMap) docs.push_back(it.second); return docs; @@ -710,7 +678,7 @@ Document* Application::openDocument(const char * FileName, DocumentCreateFlags c } Document *Application::getDocumentByPath(const char *path, PathMatchMode checkCanonical) const { - if(!path || !path[0]) + if(Base::Tools::isNullOrEmpty(path)) return nullptr; if(DocFileMap.empty()) { for(const auto &v : DocMap) { @@ -988,8 +956,8 @@ Document* Application::openDocumentPrivate(const char * FileName, // objects. To partially solve this problem, we do not // close and reopen the document immediately here, but // add it to _pendingDocsReopen to delay reloading. - for(auto obj : doc->getObjects()) - objNames.emplace_back(obj->getNameInDocument()); + for(auto obj2 : doc->getObjects()) + objNames.emplace_back(obj2->getNameInDocument()); _pendingDocMap[doc->FileName.getValue()] = std::move(objNames); break; } @@ -1004,10 +972,13 @@ Document* Application::openDocumentPrivate(const char * FileName, } } - if(!isMainDoc) + if (!isMainDoc) { return nullptr; - else if(doc) + } + + if (doc) { return doc; + } } std::string name; @@ -1088,10 +1059,7 @@ void Application::setActiveDocument(const char* Name) return; } - std::map::iterator pos; - pos = DocMap.find(Name); - - if (pos != DocMap.end()) { + if (const auto pos = DocMap.find(Name); pos != DocMap.end()) { setActiveDocument(pos->second); } else { @@ -1196,7 +1164,7 @@ std::string Application::getResourceDir() { #ifdef RESOURCEDIR // #6892: Conda may inject null characters => remove them - std::string path = std::string(RESOURCEDIR).c_str(); + auto path = std::string(RESOURCEDIR); path += PATHSEP; QDir dir(QString::fromStdString(path)); if (dir.isAbsolute()) @@ -1211,7 +1179,7 @@ std::string Application::getLibraryDir() { #ifdef LIBRARYDIR // #6892: Conda may inject null characters => remove them - std::string path = std::string(LIBRARYDIR).c_str(); + auto path = std::string(LIBRARYDIR); QDir dir(QString::fromStdString(path)); if (dir.isAbsolute()) return path; @@ -1225,7 +1193,7 @@ std::string Application::getHelpDir() { #ifdef DOCDIR // #6892: Conda may inject null characters => remove them - std::string path = std::string(DOCDIR).c_str(); + auto path = std::string(DOCDIR); path += PATHSEP; QDir dir(QString::fromStdString(path)); if (dir.isAbsolute()) @@ -1246,7 +1214,7 @@ int Application::checkLinkDepth(int depth, MessageOption option) } if (depth > _objCount + 2) { - const char *msg = "Link recursion limit reached. " + auto msg = "Link recursion limit reached. " "Please check for cyclic reference."; switch (option) { case MessageOption::Quiet: @@ -1269,7 +1237,7 @@ std::set Application::getLinksTo( if(!obj) { for(auto &v : DocMap) { v.second->getLinksTo(links,obj,options,maxCount); - if(maxCount && (int)links.size()>=maxCount) + if(maxCount && static_cast(links.size())>=maxCount) break; } } else { @@ -1277,7 +1245,7 @@ std::set Application::getLinksTo( for(auto o : obj->getInList()) { if(o && o->isAttachedToDocument() && docs.insert(o->getDocument()).second) { o->getDocument()->getLinksTo(links,obj,options,maxCount); - if(maxCount && (int)links.size()>=maxCount) + if(maxCount && static_cast(links.size())>=maxCount) break; } } @@ -1302,10 +1270,8 @@ ParameterManager & Application::GetUserParameter() ParameterManager * Application::GetParameterSet(const char* sName) const { auto it = mpcPramManager.find(sName); - if ( it != mpcPramManager.end() ) - return it->second; - else - return nullptr; + + return it != mpcPramManager.end() ? it->second : nullptr; } const std::map> & @@ -1346,7 +1312,7 @@ Base::Reference Application::GetParameterGroupByPath(const char* cName.erase(0,pos+1); // test if name is valid - auto It = mpcPramManager.find(cTemp.c_str()); + auto It = mpcPramManager.find(cTemp); if (It == mpcPramManager.end()) throw Base::ValueError("Application::GetParameterGroupByPath() unknown parameter set name specified"); @@ -1413,8 +1379,10 @@ std::vector Application::getImportModules(const char* Type) const std::vector Application::getImportModules() const { std::vector modules; - for (const auto & it : _mImportTypes) + modules.reserve(_mImportTypes.size()); + for (const auto& it : _mImportTypes) { modules.push_back(it.module); + } std::sort(modules.begin(), modules.end()); modules.erase(std::unique(modules.begin(), modules.end()), modules.end()); return modules; @@ -1536,8 +1504,10 @@ std::vector Application::getExportModules(const char* Type) const std::vector Application::getExportModules() const { std::vector modules; - for (const auto & it : _mExportTypes) + modules.reserve(_mExportTypes.size()); + for (const auto& it : _mExportTypes) { modules.push_back(it.module); + } std::sort(modules.begin(), modules.end()); modules.erase(std::unique(modules.begin(), modules.end()), modules.end()); return modules; @@ -1853,11 +1823,11 @@ void printBacktrace(size_t skip=0) std::stringstream str; if (status == 0) { void* offset = (void*)((char*)callstack[i] - (char*)info.dli_saddr); - str << "#" << (i-skip) << " " << callstack[i] << " in " << demangled << " from " << info.dli_fname << "+" << offset << std::endl; + str << "#" << (i-skip) << " " << callstack[i] << " in " << demangled << " from " << info.dli_fname << "+" << offset << '\n'; free(demangled); } else { - str << "#" << (i-skip) << " " << symbols[i] << std::endl; + str << "#" << (i-skip) << " " << symbols[i] << '\n'; } // cannot directly print to cerr when using --write-log @@ -1886,19 +1856,19 @@ void segmentation_fault_handler(int sig) #else switch (sig) { case SIGSEGV: - std::cerr << "Illegal storage access..." << std::endl; + std::cerr << "Illegal storage access..." << '\n'; #if !defined(_DEBUG) throw Base::AccessViolation("Illegal storage access! Please save your work under a new file name and restart the application!"); #endif break; case SIGABRT: - std::cerr << "Abnormal program termination..." << std::endl; + std::cerr << "Abnormal program termination..." << '\n'; #if !defined(_DEBUG) throw Base::AbnormalProgramTermination("Break signal occurred"); #endif break; default: - std::cerr << "Unknown error occurred..." << std::endl; + std::cerr << "Unknown error occurred..." << '\n'; break; } #endif // FC_OS_LINUX @@ -1906,12 +1876,12 @@ void segmentation_fault_handler(int sig) void unhandled_exception_handler() { - std::cerr << "Terminating..." << std::endl; + std::cerr << "Terminating..." << '\n'; } void unexpection_error_handler() { - std::cerr << "Unexpected error occurred..." << std::endl; + std::cerr << "Unexpected error occurred..." << '\n'; // try to throw an exception and give the user chance to save their work #if !defined(_DEBUG) throw Base::AbnormalProgramTermination("Unexpected error occurred! Please save your work under a new file name and restart the application!"); @@ -2370,21 +2340,21 @@ void parseProgramOptions(int ac, char ** av, const string& exe, variables_map& v } catch (const std::exception& e) { std::stringstream str; - str << e.what() << endl << endl << visible << endl; + str << e.what() << '\n' << '\n' << visible << '\n'; throw Base::UnknownProgramOption(str.str()); } catch (...) { std::stringstream str; - str << "Wrong or unknown option, bailing out!" << endl << endl << visible << endl; + str << "Wrong or unknown option, bailing out!" << '\n' << '\n' << visible << '\n'; throw Base::UnknownProgramOption(str.str()); } if (vm.count("help")) { std::stringstream str; - str << exe << endl << endl; - str << "For a detailed description see https://www.freecad.org/wiki/Start_up_and_Configuration" << endl<() << "'" << endl; + << vm["response-file"].as() << "'" << '\n'; throw Base::UnknownProgramOption(str.str()); } // Read the whole file into a string @@ -2404,10 +2374,10 @@ void parseProgramOptions(int ac, char ** av, const string& exe, variables_map& v // Split the file content char_separator sep(" \n\r"); tokenizer > tok(ss.str(), sep); - vector args; - copy(tok.begin(), tok.end(), back_inserter(args)); + vector args2; + copy(tok.begin(), tok.end(), back_inserter(args2)); // Parse the file and store the options - store( boost::program_options::command_line_parser(args). + store( boost::program_options::command_line_parser(args2). options(cmdline_options).positional(p).extra_parser(Util::customSyntax).run(), vm); } } @@ -2417,7 +2387,7 @@ void processProgramOptions(const variables_map& vm, std::map Mods = vm["module-path"].as< vector >(); + auto Mods = vm["module-path"].as< vector >(); string temp; for (const auto & It : Mods) temp += It + ";"; @@ -2458,7 +2428,7 @@ void processProgramOptions(const variables_map& vm, std::map Paths = vm["python-path"].as< vector >(); + auto Paths = vm["python-path"].as< vector >(); for (const auto & It : Paths) Base::Interpreter().addPythonPath(It.c_str()); } @@ -2474,7 +2444,7 @@ void processProgramOptions(const variables_map& vm, std::map files(vm["input-file"].as< vector >()); + auto files(vm["input-file"].as< vector >()); int OpenFileCount=0; for (const auto & It : files) { @@ -2536,25 +2506,25 @@ void processProgramOptions(const variables_map& vm, std::map(); + auto configKey = vm["get-config"].as(); std::stringstream str; std::map::iterator pos; pos = mConfig.find(configKey); if (pos != mConfig.end()) { str << pos->second; } - str << std::endl; + str << '\n'; throw Base::ProgramInformation(str.str()); } if (vm.count("set-config")) { - std::vector configKeyValue = vm["set-config"].as< std::vector >(); + auto configKeyValue = vm["set-config"].as< std::vector >(); for (const auto& it : configKeyValue) { auto pos = it.find('='); if (pos != std::string::npos) { @@ -2653,9 +2623,9 @@ void Application::initConfig(int argc, char ** argv) // because the (external) interpreter is already initialized. // Therefore we use a workaround as described in https://stackoverflow.com/a/57019607 - PyObject *sysModules = PyImport_GetModuleDict(); + PyObject* sysModules = PyImport_GetModuleDict(); - const char *moduleName = "FreeCAD"; + auto moduleName = "FreeCAD"; PyImport_AddModule(moduleName); ApplicationMethods = Application::Methods; PyObject *pyModule = init_freecad_module(); @@ -2700,10 +2670,10 @@ void Application::initConfig(int argc, char ** argv) _pConsoleObserverFile = nullptr; // Banner =========================================================== - if (!(mConfig["RunMode"] == "Cmd")) { + if (mConfig["RunMode"] != "Cmd") { // Remove banner if FreeCAD is invoked via the -c command as regular // Python interpreter - if (!(mConfig["Verbose"] == "Strict")) + if (mConfig["Verbose"] != "Strict") Base::Console().Message("%s %s, Libs: %s.%s.%s%sR%s\n%s", mConfig["ExeName"].c_str(), mConfig["ExeVersion"].c_str(), @@ -2725,8 +2695,8 @@ void Application::initConfig(int argc, char ** argv) if (SafeMode::SafeModeEnabled()) { Base::Console().Message("FreeCAD is running in _SAFE_MODE_.\n" - "Safe mode temporarily disables your configurations and " - "addons. Restart the application to exit safe mode.\n\n"); + "Safe mode temporarily disables your configurations and " + "addons. Restart the application to exit safe mode.\n\n"); } } LoadParameters(); @@ -2747,12 +2717,12 @@ void Application::initConfig(int argc, char ** argv) #ifdef FC_DEBUG if (v.second>=0) { hasDefault = true; - Base::Console().SetDefaultLogLevel(v.second); + Base::Console().SetDefaultLogLevel(static_cast(v.second)); } #endif } else { - *Base::Console().GetLogLevel(v.first.c_str()) = v.second; + *Base::Console().GetLogLevel(v.first.c_str()) = static_cast(v.second); } } @@ -2829,18 +2799,18 @@ void Application::initApplication() new Base::ScriptProducer( "FreeCADTest", FreeCADTest ); // creating the application - if (!(mConfig["Verbose"] == "Strict")) + if (mConfig["Verbose"] != "Strict") Base::Console().Log("Create Application\n"); Application::_pcSingleton = new Application(mConfig); // set up Unit system default ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath ("User parameter:BaseApp/Preferences/Units"); - Base::UnitsApi::setSchema((Base::UnitSystem)hGrp->GetInt("UserSchema",0)); - Base::UnitsApi::setDecimals(hGrp->GetInt("Decimals", Base::UnitsApi::getDecimals())); + UnitsApi::setSchema(static_cast(hGrp->GetInt("UserSchema", 0))); + UnitsApi::setDecimals(static_cast(hGrp->GetInt("Decimals", UnitsApi::getDecimals()))); // In case we are using fractional inches, get user setting for min unit - int denom = hGrp->GetInt("FracInch", Base::QuantityFormat::getDefaultDenominator()); + int denom = static_cast(hGrp->GetInt("FracInch", Base::QuantityFormat::getDefaultDenominator())); Base::QuantityFormat::setDefaultDenominator(denom); @@ -2965,8 +2935,8 @@ void Application::processCmdLineFiles() } } - const std::map& cfg = Application::Config(); - std::map::const_iterator it = cfg.find("SaveFile"); + const std::map& cfg = Application::Config(); + auto it = cfg.find("SaveFile"); if (it != cfg.end()) { std::string output = it->second; output = Base::Tools::escapeEncodeFilename(output); @@ -3045,13 +3015,13 @@ void Application::LoadParameters() _pcUserParamMngr->SetSerializer(new ParameterSerializer(mConfig["UserParameter"])); try { - if (_pcSysParamMngr->LoadOrCreateDocument() && !(mConfig["Verbose"] == "Strict")) { + if (_pcSysParamMngr->LoadOrCreateDocument() && mConfig["Verbose"] != "Strict") { // Configuration file optional when using as Python module if (!Py_IsInitialized()) { Base::Console().Warning(" Parameter does not exist, writing initial one\n"); Base::Console().Message(" This warning normally means that FreeCAD is running for the first time\n" - " or the configuration was deleted or moved. FreeCAD is generating the standard\n" - " configuration.\n"); + " or the configuration was deleted or moved. FreeCAD is generating the standard\n" + " configuration.\n"); } } } @@ -3064,10 +3034,10 @@ void Application::LoadParameters() } try { - if (_pcUserParamMngr->LoadOrCreateDocument() && !(mConfig["Verbose"] == "Strict")) { + if (_pcUserParamMngr->LoadOrCreateDocument() && mConfig["Verbose"] != "Strict") { // The user parameter file doesn't exist. When an alternative parameter file is offered // this will be used. - std::map::iterator it = mConfig.find("UserParameterTemplate"); + auto it = mConfig.find("UserParameterTemplate"); if (it != mConfig.end()) { QString path = QString::fromUtf8(it->second.c_str()); if (QDir(path).isRelative()) { @@ -3084,8 +3054,8 @@ void Application::LoadParameters() if (!Py_IsInitialized()) { Base::Console().Warning(" User settings do not exist, writing initial one\n"); Base::Console().Message(" This warning normally means that FreeCAD is running for the first time\n" - " or your configuration was deleted or moved. The system defaults\n" - " will be automatically generated for you.\n"); + " or your configuration was deleted or moved. The system defaults\n" + " will be automatically generated for you.\n"); } } } @@ -3210,12 +3180,7 @@ void getOldDataLocation(std::map& mConfig, std::vector< */ QString findUserHomePath(const QString& userHome) { - if (userHome.isEmpty()) { - return getUserHome(); - } - else { - return userHome; - } + return userHome.isEmpty() ? getUserHome() : userHome; } /*! @@ -3516,12 +3481,11 @@ std::string Application::FindHomePath(const char* call) // FreeCAD shared library. if (!Py_IsInitialized()) { uint32_t sz = 0; - char *buf; - _NSGetExecutablePath(NULL, &sz); //function only returns "sz" if first arg is to small to hold value - buf = new char[++sz]; + // function only returns "sz" if first arg is to small to hold value + _NSGetExecutablePath(nullptr, &sz); - if (_NSGetExecutablePath(buf, &sz) == 0) { + if (const auto buf = new char[++sz]; _NSGetExecutablePath(buf, &sz) == 0) { char resolved[PATH_MAX]; char* path = realpath(buf, resolved); delete [] buf; diff --git a/src/Gui/Application.cpp b/src/Gui/Application.cpp index 5a13553bf4..c8827f4b01 100644 --- a/src/Gui/Application.cpp +++ b/src/Gui/Application.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -707,6 +708,11 @@ void Application::importFrom(const char* FileName, const char* DocName, const ch // load the file with the module if (File.hasExtension("FCStd")) { Command::doCommand(Command::App, "%s.open(u\"%s\")", Module, unicodepath.c_str()); + setStatus(UserInitiatedOpenDocument, false); + App::Document* doc = App::GetApplication().getActiveDocument(); + checkPartialRestore(doc); + checkRestoreError(doc); + checkForRecomputes(); if (activeDocument()) { activeDocument()->setModified(false); } @@ -1007,6 +1013,30 @@ void Application::checkForRecomputes() { "Please check report view for more details.")); } +void Application::checkPartialRestore(App::Document* doc) +{ + if (doc && doc->testStatus(App::Document::PartialRestore)) { + QMessageBox::critical( + getMainWindow(), + QObject::tr("Error"), + QObject::tr("There were errors while loading the file. Some data might have been " + "modified or not recovered at all. Look in the report view for more " + "specific information about the objects involved.")); + } +} + +void Application::checkRestoreError(App::Document* doc) +{ + if (doc && doc->testStatus(App::Document::RestoreError)) { + QMessageBox::critical( + getMainWindow(), + QObject::tr("Error"), + QObject::tr("There were serious errors while loading the file. Some data might have " + "been modified or not recovered at all. Saving the project will most " + "likely result in loss of data.")); + } +} + void Application::slotActiveDocument(const App::Document& Doc) { std::map::iterator doc = d->documents.find(&Doc); diff --git a/src/Gui/Application.h b/src/Gui/Application.h index e358e6008c..54f7c4dd15 100644 --- a/src/Gui/Application.h +++ b/src/Gui/Application.h @@ -79,6 +79,10 @@ public: App::Document *reopen(App::Document *doc); /// Prompt about recomputing if needed static void checkForRecomputes(); + /// Prompt about PartialRestore + void checkPartialRestore(App::Document* doc); + /// Prompt for Errors on open + void checkRestoreError(App::Document* doc); //@} diff --git a/src/Gui/CommandDoc.cpp b/src/Gui/CommandDoc.cpp index 6d9274b69a..8ed8880d7e 100644 --- a/src/Gui/CommandDoc.cpp +++ b/src/Gui/CommandDoc.cpp @@ -96,26 +96,6 @@ StdCmdOpen::StdCmdOpen() void StdCmdOpen::activated(int iMsg) { - // clang-format off - auto checkPartialRestore = [](App::Document* doc) { - if (doc && doc->testStatus(App::Document::PartialRestore)) { - QMessageBox::critical(getMainWindow(), QObject::tr("Error"), - QObject::tr("There were errors while loading the file. Some data might have been " - "modified or not recovered at all. Look in the report view for more " - "specific information about the objects involved.")); - } - }; - - auto checkRestoreError = [](App::Document* doc) { - if (doc && doc->testStatus(App::Document::RestoreError)) { - QMessageBox::critical(getMainWindow(), QObject::tr("Error"), - QObject::tr("There were serious errors while loading the file. Some data might have " - "been modified or not recovered at all. Saving the project will most " - "likely result in loss of data.")); - } - }; - // clang-format on - Q_UNUSED(iMsg); // fill the list of registered endings @@ -183,8 +163,8 @@ void StdCmdOpen::activated(int iMsg) App::Document *doc = App::GetApplication().getActiveDocument(); - checkPartialRestore(doc); - checkRestoreError(doc); + getGuiApplication()->checkPartialRestore(doc); + getGuiApplication()->checkRestoreError(doc); } } } diff --git a/src/Gui/Dialogs/DlgAbout.cpp b/src/Gui/Dialogs/DlgAbout.cpp index 45b253a232..ca6c8b9011 100644 --- a/src/Gui/Dialogs/DlgAbout.cpp +++ b/src/Gui/Dialogs/DlgAbout.cpp @@ -580,6 +580,7 @@ void AboutDialog::copyToClipboard() QString point = QString::fromStdString(config["BuildVersionPoint"]); QString suffix = QString::fromStdString(config["BuildVersionSuffix"]); QString build = QString::fromStdString(config["BuildRevision"]); + QString buildDate = QString::fromStdString(config["BuildRevisionDate"]); QString deskEnv = QProcessEnvironment::systemEnvironment().value(QStringLiteral("XDG_CURRENT_DESKTOP"), @@ -628,6 +629,7 @@ void AboutDialog::copyToClipboard() str << " Snap " << snap; } str << '\n'; + str << "Build date: " << buildDate << "\n"; #if defined(_DEBUG) || defined(DEBUG) str << "Build type: Debug\n"; diff --git a/src/Mod/AddonManager/update_all.ui b/src/Mod/AddonManager/update_all.ui index 266b779f9a..27672917d5 100644 --- a/src/Mod/AddonManager/update_all.ui +++ b/src/Mod/AddonManager/update_all.ui @@ -26,8 +26,14 @@ + + Qt::ScrollBarAlwaysOff + + + QAbstractScrollArea::AdjustToContents + - QAbstractItemView::EditTrigger::NoEditTriggers + QAbstractItemView::NoEditTriggers false @@ -39,10 +45,10 @@ false - QAbstractItemView::SelectionMode::NoSelection + QAbstractItemView::NoSelection - QAbstractItemView::SelectionBehavior::SelectRows + QAbstractItemView::SelectItems false @@ -60,11 +66,8 @@ - - Qt::Orientation::Horizontal - - QDialogButtonBox::StandardButton::Cancel + QDialogButtonBox::Cancel diff --git a/src/Mod/BIM/nativeifc/ifc_openshell.py b/src/Mod/BIM/nativeifc/ifc_openshell.py index c0553efb7d..f38b866ee4 100644 --- a/src/Mod/BIM/nativeifc/ifc_openshell.py +++ b/src/Mod/BIM/nativeifc/ifc_openshell.py @@ -26,6 +26,8 @@ import FreeCAD import FreeCADGui +from packaging.version import Version +from addonmanager_utilities import create_pip_call translate = FreeCAD.Qt.translate QT_TRANSLATE_NOOP = FreeCAD.Qt.QT_TRANSLATE_NOOP @@ -49,8 +51,7 @@ class IFC_UpdateIOS: avail = self.get_avail_version() if avail: if version: - comp = self.compare_versions(avail, version) - if comp > 0: + if Version(version) < Version(avail): self.show_dialog("update", avail) else: self.show_dialog("uptodate") @@ -92,6 +93,7 @@ class IFC_UpdateIOS: if mode in ["update", "install"]: result = self.install() if result: + FreeCAD.Console.PrintLog(f"{result.stdout}\n") text = translate("BIM", "IfcOpenShell update successfully installed.") buttons = QtGui.QMessageBox.Ok reply = QtGui.QMessageBox.information(None, title, text, buttons) @@ -104,7 +106,7 @@ class IFC_UpdateIOS: from PySide import QtCore, QtGui QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) vendor_path = utils.get_pip_target_directory() - args = ["install", "--disable-pip-version-check", "--target", vendor_path, "ifcopenshell"] + args = ["install", "--upgrade", "--disable-pip-version-check", "--target", vendor_path, "ifcopenshell"] result = self.run_pip(args) QtGui.QApplication.restoreOverrideCursor() return result @@ -115,14 +117,17 @@ class IFC_UpdateIOS: import addonmanager_utilities as utils import freecad.utils - cmd = [freecad.utils.get_python_exe(), "-m", "pip"] - cmd.extend(args) + from subprocess import CalledProcessError + + cmd = create_pip_call(args) result = None try: result = utils.run_interruptable_subprocess(cmd) - except: + except CalledProcessError as pe: + FreeCAD.Console.PrintError(pe.stderr) + except Exception as e: text = translate("BIM","Unable to run pip. Please ensure pip is installed on your system.") - FreeCAD.Console.PrintError(text + "\n") + FreeCAD.Console.PrintError(f"{text} {str(e)}\n") return result @@ -130,19 +135,19 @@ class IFC_UpdateIOS: """Retrieves the current ifcopenshell version""" import addonmanager_utilities as utils + from packaging.version import InvalidVersion + try: import ifcopenshell - version = ifcopenshell.version + version = ifcopenshell.version + try: + Version(version) + except InvalidVersion: + FreeCAD.Console.PrintWarning(f"Invalid IfcOpenShell version: {version}\n") + version = "" except: version = "" - if version.startswith("v"): - # this is a pip version - vendor_path = utils.get_pip_target_directory() - result = self.run_pip(["list", "--path", vendor_path]) - if result: - result = result.stdout.split() - if "ifcopenshell" in result: - version = result[result.index("ifcopenshell")+1] + return version @@ -159,31 +164,6 @@ class IFC_UpdateIOS: return None - def compare_versions(self, v1, v2): - """Compare two version strings in the form '0.7.0' or v0.7.0""" - - # code from https://www.geeksforgeeks.org/compare-two-version-numbers - - arr1 = v1.replace("v","").split(".") - arr2 = v2.replace("v","").split(".") - n = len(arr1) - m = len(arr2) - arr1 = [int(i) for i in arr1] - arr2 = [int(i) for i in arr2] - if n > m: - for i in range(m, n): - arr2.append(0) - elif m > n: - for i in range(n, m): - arr1.append(0) - for i in range(len(arr1)): - if arr1[i] > arr2[i]: - return 1 - elif arr2[i] > arr1[i]: - return -1 - return 0 - - FreeCADGui.addCommand("IFC_UpdateIOS", IFC_UpdateIOS()) diff --git a/src/Mod/Draft/draftguitools/gui_trimex.py b/src/Mod/Draft/draftguitools/gui_trimex.py index f6a98143d4..4cfab71cac 100644 --- a/src/Mod/Draft/draftguitools/gui_trimex.py +++ b/src/Mod/Draft/draftguitools/gui_trimex.py @@ -158,7 +158,6 @@ class Trimex(gui_base_original.Modifier): self.finish() _err(translate("draft", "Trimex is not supported yet on this type of object.")) return - # self.obj.ViewObject.Visibility = False self.obj.ViewObject.LineColor = (0.5, 0.5, 0.5) self.obj.ViewObject.LineWidth = 1 self.extrudeMode = False @@ -393,7 +392,7 @@ class Trimex(gui_base_original.Modifier): newedges.append(_sh) ghost.on() - # resetting the visible edges + # resetting the edges if not reverse: li = list(range(npoint + 1, len(self.edges))) else: @@ -584,7 +583,6 @@ class Trimex(gui_base_original.Modifier): for g in self.ghost: g.finalize() if self.obj: - self.obj.ViewObject.Visibility = True if self.color: self.obj.ViewObject.LineColor = self.color if self.width: diff --git a/src/Mod/Sketcher/App/SketchObject.cpp b/src/Mod/Sketcher/App/SketchObject.cpp index 0bfa228d53..4c9b8be7fe 100644 --- a/src/Mod/Sketcher/App/SketchObject.cpp +++ b/src/Mod/Sketcher/App/SketchObject.cpp @@ -366,10 +366,23 @@ void SketchObject::buildShape() { auto egf = ExternalGeometryFacade::getFacade(geo); if(!egf->testFlag(ExternalGeometryExtension::Defining)) continue; + auto indexedName = Data::IndexedName::fromConst("ExternalEdge", i-1); - shapes.push_back(getEdge(geo, convertSubName(indexedName, false).c_str())); - if (checkSmallEdge(shapes.back())) { - FC_WARN("Edge too small: " << indexedName); + + if (geo->isDerivedFrom()) { + Part::TopoShape vertex(TopoDS::Vertex(geo->toShape())); + if (!vertex.hasElementMap()) { + vertex.resetElementMap(std::make_shared()); + } + vertex.setElementName(Data::IndexedName::fromConst("Vertex", 1), + Data::MappedName::fromRawData(convertSubName(indexedName, false).c_str()),0L); + vertices.push_back(vertex); + vertices.back().copyElementMap(vertex, Part::OpCodes::Sketch); + } else { + shapes.push_back(getEdge(geo, convertSubName(indexedName, false).c_str())); + if (checkSmallEdge(shapes.back())) { + FC_WARN("Edge too small: " << indexedName); + } } } diff --git a/src/Mod/Start/App/CustomFolderModel.cpp b/src/Mod/Start/App/CustomFolderModel.cpp index bd7036e2e1..6225349039 100644 --- a/src/Mod/Start/App/CustomFolderModel.cpp +++ b/src/Mod/Start/App/CustomFolderModel.cpp @@ -41,6 +41,8 @@ CustomFolderModel::CustomFolderModel(QObject* parent) _customFolderDirectory = QDir(QString::fromStdString(parameterGroup->GetASCII("CustomFolder", ""))); + + _showOnlyFCStd = parameterGroup->GetBool("ShowOnlyFCStd", false); } void CustomFolderModel::loadCustomFolder() @@ -52,6 +54,11 @@ void CustomFolderModel::loadCustomFolder() "BaseApp/Preferences/Mod/Start/CustomFolder: cannot read custom folder %s\n", _customFolderDirectory.absolutePath().toStdString().c_str()); } + + if (_showOnlyFCStd) { + _customFolderDirectory.setNameFilters(QStringList() << QStringLiteral("*.FCStd")); + } + auto entries = _customFolderDirectory.entryList(QDir::Filter::Files | QDir::Filter::Readable, QDir::SortFlag::Name); for (const auto& entry : entries) { diff --git a/src/Mod/Start/App/CustomFolderModel.h b/src/Mod/Start/App/CustomFolderModel.h index d4c132708d..3901e1e01d 100644 --- a/src/Mod/Start/App/CustomFolderModel.h +++ b/src/Mod/Start/App/CustomFolderModel.h @@ -46,6 +46,7 @@ public: private: QDir _customFolderDirectory; + bool _showOnlyFCStd; // Show only FreeCAD files }; } // namespace Start diff --git a/src/Mod/Start/Gui/DlgStartPreferences.ui b/src/Mod/Start/Gui/DlgStartPreferences.ui index 2dcf6df394..20f0e5bd1a 100644 --- a/src/Mod/Start/Gui/DlgStartPreferences.ui +++ b/src/Mod/Start/Gui/DlgStartPreferences.ui @@ -20,6 +20,20 @@ Contents + + + + Show only FreeCAD files in additional folder + + + + + + + Show examples folder contents + + + @@ -27,6 +41,22 @@ + + + + An optional custom folder to be displayed on the Start page. + + + Gui::FileChooser::Directory + + + CustomFolder + + + Mod/Start + + + @@ -49,29 +79,25 @@ - - + + - An optional custom folder to be displayed on the Start page. + If the additional folder contents should include only .FCStd files - - Gui::FileChooser::Directory + + Qt::RightToLeft + + + - CustomFolder + ShowOnlyFCStd Mod/Start - - - - Show examples folder contents - - - diff --git a/src/Mod/Start/Gui/DlgStartPreferencesImp.cpp b/src/Mod/Start/Gui/DlgStartPreferencesImp.cpp index 0d842d1b23..84b17fae2d 100644 --- a/src/Mod/Start/Gui/DlgStartPreferencesImp.cpp +++ b/src/Mod/Start/Gui/DlgStartPreferencesImp.cpp @@ -53,6 +53,7 @@ void DlgStartPreferencesImp::saveSettings() ui->fileChooserCustomFolder->onSave(); ui->checkBoxShowExamples->onSave(); ui->checkBoxCloseAfterLoading->onSave(); + ui->checkBoxShowOnlyFCStd->onSave(); } void DlgStartPreferencesImp::loadSettings() @@ -60,6 +61,7 @@ void DlgStartPreferencesImp::loadSettings() ui->fileChooserCustomFolder->onRestore(); ui->checkBoxShowExamples->onRestore(); ui->checkBoxCloseAfterLoading->onRestore(); + ui->checkBoxShowOnlyFCStd->onRestore(); } /** diff --git a/src/Mod/Start/Gui/FileCardView.cpp b/src/Mod/Start/Gui/FileCardView.cpp index 5b2ab5687d..343a644d9c 100644 --- a/src/Mod/Start/Gui/FileCardView.cpp +++ b/src/Mod/Start/Gui/FileCardView.cpp @@ -39,7 +39,7 @@ FileCardView::FileCardView(QWidget* parent) sizePolicy.setHeightForWidth(true); setSizePolicy(sizePolicy); setHorizontalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff); - setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded); setViewMode(QListView::ViewMode::IconMode); setFlow(QListView::Flow::LeftToRight); setResizeMode(QListView::ResizeMode::Adjust); diff --git a/src/Mod/TechDraw/App/PreCompiled.h b/src/Mod/TechDraw/App/PreCompiled.h index ebf3abca7b..da9dad16b5 100644 --- a/src/Mod/TechDraw/App/PreCompiled.h +++ b/src/Mod/TechDraw/App/PreCompiled.h @@ -48,6 +48,10 @@ #include #include #include +#include +#include +#include +#include #include // Qt diff --git a/tools/lint/generic_checks.py b/tools/lint/generic_checks.py new file mode 100755 index 0000000000..b7eb19675f --- /dev/null +++ b/tools/lint/generic_checks.py @@ -0,0 +1,159 @@ +import argparse +import glob +import os +from utils import ( + add_common_arguments, + init_environment, + write_file, + emit_problem_matchers, +) + + +def check_line_endings(file_paths): + """Check if files have non-Unix (CRLF or CR) line endings. + + Returns a dict mapping file path to an issue description. + """ + issues = {} + for path in file_paths: + try: + with open(path, "rb") as f: + content = f.read() + if b"\r\n" in content or (b"\r" in content and b"\n" not in content): + issues[path] = "File has non-Unix line endings." + except Exception as e: + issues[path] = f"Error reading file: {e}" + return issues + + +def check_trailing_whitespace(file_paths): + """Check for trailing whitespace in files. + + Returns a dict mapping file paths to a list of line numbers with trailing whitespace. + """ + issues = {} + for path in file_paths: + try: + with open(path, "r", encoding="utf-8") as f: + lines = files.read().splitlines() + error_lines = [idx if line.endswith(" ") for idx, line in enumerate(lines, start=1)] + if error_lines: + issues[path] = error_lines + except Exception as e: + issues[path] = f"Error reading file: {e}" + return issues + + +def check_tabs(file_paths): + """Check for usage of tab characters in files. + + Returns a dict mapping file paths to a list of line numbers containing tabs. + """ + issues = {} + for path in file_paths: + try: + with open(path, "r", encoding="utf-8") as f: + lines = f.readlines() + tab_lines = [idx if "\t" in line for idx, line in enumerate(lines, start=1)] + if tab_lines: + issues[path] = tab_lines + except Exception as e: + issues[path] = f"Error reading file: {e}" + return issues + + +def format_report(section_title, issues): + """Format a report section with a Markdown details block.""" + report = [] + if issues: + count = len(issues) + report.append( + f"
:information_source: Found {count} issue(s) in {section_title}\n" + ) + report.append("```") + for file, details in issues.items(): + report.append(f"{file}: {details}") + report.append("```\n
\n") + else: + report.append(f":heavy_check_mark: No issues found in {section_title}\n") + return "\n".join(report) + + +def main(): + parser = argparse.ArgumentParser(description="Run formatting lint checks.") + add_common_arguments(parser) + parser.add_argument( + "--lineendings-check", + action="store_true", + help="Enable check for non-Unix line endings.", + ) + parser.add_argument( + "--whitespace-check", + action="store_true", + help="Enable check for trailing whitespace.", + ) + parser.add_argument( + "--tabs-check", action="store_true", help="Enable check for tab characters." + ) + args = parser.parse_args() + init_environment(args) + + file_list = glob.glob(args.files, recursive=True) + file_list = [f for f in file_list if os.path.isfile(f)] + + report_sections = [] + + # Check non-Unix line endings. + if args.lineendings_check: + le_issues = check_line_endings(file_list) + for file, detail in le_issues.items(): + print(f"::warning file={file},title={file}::{detail}") + report_sections.append(format_report("Non-Unix Line Endings", le_issues)) + + # Check trailing whitespace. + if args.whitespace_check: + ws_issues = check_trailing_whitespace(file_list) + if ws_issues: + ws_output_lines = [] + for file, details in ws_issues.items(): + if isinstance(details, list): + for line in details: + ws_output_lines.append(f"{file}:{line}: trailing whitespace") + else: + ws_output_lines.append(f"{file}: {details}") + ws_output = "\n".join(ws_output_lines) + + ws_log_file = os.path.join(args.log_dir, "whitespace.log") + write_file(ws_log_file, ws_output) + emit_problem_matchers( + ws_log_file, "grepMatcherWarning.json", "grepMatcher-warning" + ) + report_sections.append(format_report("Trailing Whitespace", ws_issues)) + + # Check tab usage. + if args.tabs_check: + tab_issues = check_tabs(file_list) + if tab_issues: + tab_output_lines = [] + for file, details in tab_issues.items(): + if isinstance(details, list): + for line in details: + tab_output_lines.append(f"{file}:{line}: contains tab") + else: + tab_output_lines.append(f"{file}: {details}") + tab_output = "\n".join(tab_output_lines) + + tab_log_file = os.path.join(args.log_dir, "tabs.log") + write_file(tab_log_file, tab_output) + emit_problem_matchers( + tab_log_file, "grepMatcherWarning.json", "grepMatcher-warning" + ) + report_sections.append(format_report("Tab Usage", tab_issues)) + + report_content = "\n".join(report_sections) + write_file(args.report_file, report_content) + print("Lint report generated at:", args.report_file) + + +if __name__ == "__main__": + main() diff --git a/tools/lint/utils.py b/tools/lint/utils.py new file mode 100644 index 0000000000..26cc4186a5 --- /dev/null +++ b/tools/lint/utils.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +import os +import sys +import logging +import subprocess +import argparse +from typing import Tuple + + +def run_command(cmd, check=False) -> Tuple[str, str, int]: + """ + Run a command using subprocess.run and return stdout, stderr, and exit code. + """ + try: + result = subprocess.run( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=check, text=True + ) + return result.stdout, result.stderr, result.returncode + except subprocess.CalledProcessError as e: + return e.stdout, e.stderr, e.returncode + + +def setup_logger(verbose: bool): + """ + Setup the logging level based on the verbose flag. + """ + level = logging.DEBUG if verbose else logging.INFO + logging.basicConfig(level=level, format="%(levelname)s: %(message)s") + + +def write_file(file_path: str, content: str): + """Write content to the specified file.""" + with open(file_path, "w", encoding="utf-8") as f: + f.write(content) + logging.info("Wrote file: %s", file_path) + + +def append_file(file_path: str, content: str): + """Append content to the specified file.""" + with open(file_path, "a", encoding="utf-8") as f: + f.write(content + "\n") + logging.info("Appended content to file: %s", file_path) + + +def emit_problem_matchers(log_path: str, matcher_filename: str, remove_owner: str): + """ + Emit GitHub Actions problem matcher commands using the given matcher file. + + This function will: + 1. Check if the log file exists. + 2. Use the RUNNER_WORKSPACE environment variable to construct the matcher path. + 3. Print the add-matcher command, then the log content, then the remove-matcher command. + """ + if os.path.isfile(log_path): + runner_workspace = os.getenv("RUNNER_WORKSPACE") + matcher_path = os.path.join( + runner_workspace, "FreeCAD", ".github", "problemMatcher", matcher_filename + ) + print(f"::add-matcher::{matcher_path}") + with open(log_path, "r", encoding="utf-8") as f: + sys.stdout.write(f.read()) + print(f"::remove-matcher owner={remove_owner}::") + + +def add_common_arguments(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: + """ + Add common command-line arguments shared between tools. + """ + parser.add_argument( + "--files", + required=True, + help="A space-separated list or glob pattern of files to check.", + ) + parser.add_argument( + "--log-dir", required=True, help="Directory where log files will be written." + ) + parser.add_argument( + "--report-file", + required=True, + help="Path to the Markdown report file to append results.", + ) + parser.add_argument("--verbose", action="store_true", help="Enable verbose output.") + return parser + + +def init_environment(args: argparse.Namespace): + """ + Perform common initialization tasks: + - Set up logging. + - Create the log directory. + - Create the directory for the report file. + """ + setup_logger(args.verbose) + os.makedirs(args.log_dir, exist_ok=True) + os.makedirs(os.path.dirname(args.report_file), exist_ok=True)