diff --git a/.github/workflows/CI_cleanup.yml b/.github/workflows/CI_cleanup.yml index a17ac554ef..bc81055b25 100644 --- a/.github/workflows/CI_cleanup.yml +++ b/.github/workflows/CI_cleanup.yml @@ -58,7 +58,7 @@ jobs: logdir: /tmp/log/ steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit 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 3d187e2ad2..d5607b9f73 100644 --- a/.github/workflows/auto-close_stale_issues_and_pull-requests.yml +++ b/.github/workflows/auto-close_stale_issues_and_pull-requests.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 38795742c1..6c34139654 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -64,7 +64,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/codeql_cpp.yml b/.github/workflows/codeql_cpp.yml index 366565db4b..e8b528771a 100644 --- a/.github/workflows/codeql_cpp.yml +++ b/.github/workflows/codeql_cpp.yml @@ -68,7 +68,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index bb23bfeb91..b25b05b707 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/issue-metrics.yml b/.github/workflows/issue-metrics.yml index 42e7d8d49a..574624919e 100644 --- a/.github/workflows/issue-metrics.yml +++ b/.github/workflows/issue-metrics.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index c5673d83a1..ca6703f629 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index fe8f207d82..2f3995ae6e 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/sub_buildPixi.yml b/.github/workflows/sub_buildPixi.yml index 00d9458bd3..759a4e3cab 100644 --- a/.github/workflows/sub_buildPixi.yml +++ b/.github/workflows/sub_buildPixi.yml @@ -54,7 +54,8 @@ jobs: CCACHE_MAXSIZE: 1G CCACHE_COMPRESS: true CCACHE_COMPRESSLEVEL: 5 - builddir: ${{ github.workspace }}/build/ + config: debug + builddir: ${{ github.workspace }}/build/debug/ cacheKey: pixi-${{ matrix.os }} logdir: ${{ github.workspace }}/logs/ reportdir: ${{ github.workspace }}/report/ @@ -70,7 +71,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit @@ -133,11 +134,11 @@ jobs: - name: CMake Configure run: | - pixi run configure-release + pixi run configure-${{ env.config }} - name: CMake Build run: | - pixi run build + pixi run build-${{ env.config }} - name: Print ccache statistics after Build run: | @@ -174,7 +175,7 @@ jobs: - name: CMake Install run: | - pixi run install + pixi run install-${{ env.config }} - name: FreeCAD CLI tests on install if: runner.os != 'Windows' diff --git a/.github/workflows/sub_buildUbuntu.yml b/.github/workflows/sub_buildUbuntu.yml index 191311d30c..78c4132e3f 100644 --- a/.github/workflows/sub_buildUbuntu.yml +++ b/.github/workflows/sub_buildUbuntu.yml @@ -72,7 +72,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/sub_buildWindows.yml b/.github/workflows/sub_buildWindows.yml index 1e1fa0680f..3fe50fa1e9 100644 --- a/.github/workflows/sub_buildWindows.yml +++ b/.github/workflows/sub_buildWindows.yml @@ -63,7 +63,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/sub_lint.yml b/.github/workflows/sub_lint.yml index ac31ef17e1..1815f6be95 100644 --- a/.github/workflows/sub_lint.yml +++ b/.github/workflows/sub_lint.yml @@ -198,7 +198,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/sub_prepare.yml b/.github/workflows/sub_prepare.yml index 45c97a16b8..7c82b5f851 100644 --- a/.github/workflows/sub_prepare.yml +++ b/.github/workflows/sub_prepare.yml @@ -81,7 +81,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/sub_weeklyBuild.yml b/.github/workflows/sub_weeklyBuild.yml index 6cd2e7e5f5..ca4df3871d 100644 --- a/.github/workflows/sub_weeklyBuild.yml +++ b/.github/workflows/sub_weeklyBuild.yml @@ -14,7 +14,7 @@ jobs: build_tag: ${{ steps.tag_build.outputs.build_tag }} steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit @@ -72,7 +72,7 @@ jobs: environment: weekly-build steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.github/workflows/sub_wrapup.yml b/.github/workflows/sub_wrapup.yml index 62ab068e5a..3dbf23fbc2 100644 --- a/.github/workflows/sub_wrapup.yml +++ b/.github/workflows/sub_wrapup.yml @@ -54,7 +54,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1 + uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 with: egress-policy: audit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d1b60a15e7..bf054f58f8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,6 +66,6 @@ repos: - id: black args: ['--line-length', '100'] - repo: https://github.com/pre-commit/mirrors-clang-format - rev: 64827eb3528d4dc019b01153e9fb79107241405f # frozen: v20.1.6 + rev: 6b9072cd80691b1b48d80046d884409fb1d962d1 # frozen: v20.1.7 hooks: - id: clang-format diff --git a/CMakePresets.json b/CMakePresets.json index 2ab2f39048..b6d9d067c2 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -121,7 +121,6 @@ "lhs": "${hostSystemName}", "rhs": "Linux" }, - "cmakeExecutable": "${sourceDir}/conda/cmake.sh", "cacheVariables": { "CMAKE_C_COMPILER": { "type": "STRING", @@ -161,7 +160,6 @@ "lhs": "${hostSystemName}", "rhs": "Darwin" }, - "cmakeExecutable": "${sourceDir}/conda/cmake.sh", "cacheVariables": { "CMAKE_IGNORE_PREFIX_PATH": { "type": "STRING", @@ -189,7 +187,6 @@ "lhs": "${hostSystemName}", "rhs": "Windows" }, - "cmakeExecutable": "${sourceDir}/conda/cmake.cmd", "cacheVariables": { "CMAKE_INSTALL_PREFIX": { "type": "FILEPATH", diff --git a/conda/cmake.cmd b/conda/cmake.cmd deleted file mode 100644 index 3f63072703..0000000000 --- a/conda/cmake.cmd +++ /dev/null @@ -1 +0,0 @@ -mamba run --live-stream -n freecad cmake %* diff --git a/conda/cmake.sh b/conda/cmake.sh deleted file mode 100755 index 958e1a8da4..0000000000 --- a/conda/cmake.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -mamba run --live-stream -n freecad cmake $@ diff --git a/conda/conda-env-qt6.yaml b/conda/conda-env-qt6.yaml deleted file mode 100644 index 77d325217e..0000000000 --- a/conda/conda-env-qt6.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: freecad -channels: -- conda-forge -dependencies: -- conda-forge/noarch::conda-libmamba-solver==24.11.1 -- conda-devenv -- mamba -- python==3.12.* -- zstd diff --git a/conda/conda-env.yaml b/conda/conda-env.yaml deleted file mode 100644 index 495ce08296..0000000000 --- a/conda/conda-env.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: freecad -channels: -- conda-forge -dependencies: -- conda-forge/noarch::conda-libmamba-solver==24.11.1 -- conda-devenv -- mamba -- python==3.11.* -- zstd==1.5.6 diff --git a/conda/environment-qt6.devenv.yml b/conda/environment-qt6.devenv.yml deleted file mode 100644 index 7a8c8dd7c8..0000000000 --- a/conda/environment-qt6.devenv.yml +++ /dev/null @@ -1,101 +0,0 @@ -name: freecad -channels: -- conda-forge -- conda-forge/label/pivy_rc -dependencies: -- conda-forge/noarch::conda-libmamba-solver==24.11.1 -- libspnav # [linux] -- kernel-headers_linux-64 # [linux and x86_64] -- libdrm-cos7-x86_64 # [linux and x86_64] -- libselinux-cos7-x86_64 # [linux and x86_64] -- libsepol-cos7-x86_64 # [linux and x86_64] -- libx11-common-cos7-x86_64 # [linux and x86_64] -- libx11-cos7-x86_64 # [linux and x86_64] -- libxau-cos7-x86_64 # [linux and x86_64] -- libxcb-cos7-x86_64 # [linux and x86_64] -- libxdamage-cos7-x86_64 # [linux and x86_64] -- libxext-cos7-x86_64 # [linux and x86_64] -- libxfixes-cos7-x86_64 # [linux and x86_64] -- libxi-cos7-x86_64 # [linux and x86_64] -- libxi-devel-cos7-x86_64 # [linux and x86_64] -- libxxf86vm-cos7-x86_64 # [linux and x86_64] -- mesa-dri-drivers-cos7-x86_64 # [linux and x86_64] -- mesa-libegl-cos7-x86_64 # [linux and x86_64] -- mesa-libegl-devel-cos7-x86_64 # [linux and x86_64] -- mesa-libgl-cos7-x86_64 # [linux and x86_64] -- mesa-libgl-devel-cos7-x86_64 # [linux and x86_64] -- pixman-cos7-x86_64 # [linux and x86_64] -- sysroot_linux-64 # [linux and x86_64] -- xorg-x11-server-common-cos7-x86_64 # [linux and x86_64] -- xorg-x11-server-xvfb-cos7-x86_64 # [linux and x86_64] -- kernel-headers_linux-aarch64 # [linux and aarch64] -- libdrm-cos7-aarch64 # [linux and aarch64] -- libglvnd-cos7-aarch64 # [linux and aarch64] -- libglvnd-glx-cos7-aarch64 # [linux and aarch64] -- libselinux-cos7-aarch64 # [linux and aarch64] -- libsepol-cos7-aarch64 # [linux and aarch64] -- libx11-common-cos7-aarch64 # [linux and aarch64] -- libx11-cos7-aarch64 # [linux and aarch64] -- libxau-cos7-aarch64 # [linux and aarch64] -- libxcb-cos7-aarch64 # [linux and aarch64] -- libxdamage-cos7-aarch64 # [linux and aarch64] -- libxext-cos7-aarch64 # [linux and aarch64] -- libxfixes-cos7-aarch64 # [linux and aarch64] -- libxi-cos7-aarch64 # [linux and aarch64] -- libxi-devel-cos7-aarch64 # [linux and aarch64] -- libxxf86vm-cos7-aarch64 # [linux and aarch64] -- mesa-dri-drivers-cos7-aarch64 # [linux and aarch64] -- mesa-khr-devel-cos7-aarch64 # [linux and aarch64] -- mesa-libegl-cos7-aarch64 # [linux and aarch64] -- mesa-libegl-devel-cos7-aarch64 # [linux and aarch64] -- mesa-libgbm-cos7-aarch64 # [linux and aarch64] -- mesa-libgl-cos7-aarch64 # [linux and aarch64] -- mesa-libgl-devel-cos7-aarch64 # [linux and aarch64] -- mesa-libglapi-cos7-aarch64 # [linux and aarch64] -- pixman-cos7-aarch64 # [linux and aarch64] -- sysroot_linux-aarch64 # [linux and aarch64] -- xorg-x11-server-common-cos7-aarch64 # [linux and aarch64] -- xorg-x11-server-xvfb-cos7-aarch64 # [linux and aarch64] -- sed # [unix] -- ccache -- clang-format -- cmake -- coin3d -- compilers -- conda -- conda-devenv -- debugpy -- doxygen -- eigen -- fmt -- freetype -- git -- gmsh -- graphviz -- hdf5 -- libboost-devel -- libcxx -- mamba -- matplotlib -- ninja -- numpy -- occt -- openssl -- pcl -- pip -- conda-forge/label/pivy_rc::pivy -- pkg-config -- ply -- pre-commit -- pybind11 -- pyside6 -- python==3.12.* -- pyyaml -- qt6-main -- smesh -- swig -- vtk==9.2.6 -- xerces-c -- yaml-cpp -- zlib -- zstd diff --git a/conda/environment.devenv.yml b/conda/environment.devenv.yml deleted file mode 100644 index 022a353490..0000000000 --- a/conda/environment.devenv.yml +++ /dev/null @@ -1,101 +0,0 @@ -name: freecad -channels: -- conda-forge -dependencies: -- conda-forge/noarch::conda-libmamba-solver==24.11.1 -- libspnav # [linux] -- kernel-headers_linux-64 # [linux and x86_64] -- libdrm-cos6-x86_64 # [linux and x86_64] -- libselinux-cos6-x86_64 # [linux and x86_64] -- libsepol-cos6-x86_64 # [linux and x86_64] -- libx11-common-cos6-x86_64 # [linux and x86_64] -- libx11-cos6-x86_64 # [linux and x86_64] -- libxau-cos6-x86_64 # [linux and x86_64] -- libxcb-cos6-x86_64 # [linux and x86_64] -- libxdamage-cos6-x86_64 # [linux and x86_64] -- libxext-cos6-x86_64 # [linux and x86_64] -- libxfixes-cos6-x86_64 # [linux and x86_64] -- libxi-cos6-x86_64 # [linux and x86_64] -- libxi-devel-cos6-x86_64 # [linux and x86_64] -- libxxf86vm-cos6-x86_64 # [linux and x86_64] -- mesa-dri-drivers-cos6-x86_64 # [linux and x86_64] -- mesa-dri1-drivers-cos6-x86_64 # [linux and x86_64] -- mesa-libegl-cos6-x86_64 # [linux and x86_64] -- mesa-libegl-devel-cos6-x86_64 # [linux and x86_64] -- mesa-libgl-cos6-x86_64 # [linux and x86_64] -- mesa-libgl-devel-cos6-x86_64 # [linux and x86_64] -- pixman-cos6-x86_64 # [linux and x86_64] -- sysroot_linux-64 # [linux and x86_64] -- xorg-x11-server-common-cos6-x86_64 # [linux and x86_64] -- xorg-x11-server-xvfb-cos6-x86_64 # [linux and x86_64] -- kernel-headers_linux-aarch64 # [linux and aarch64] -- libdrm-cos7-aarch64 # [linux and aarch64] -- libglvnd-cos7-aarch64 # [linux and aarch64] -- libglvnd-glx-cos7-aarch64 # [linux and aarch64] -- libselinux-cos7-aarch64 # [linux and aarch64] -- libsepol-cos7-aarch64 # [linux and aarch64] -- libx11-common-cos7-aarch64 # [linux and aarch64] -- libx11-cos7-aarch64 # [linux and aarch64] -- libxau-cos7-aarch64 # [linux and aarch64] -- libxcb-cos7-aarch64 # [linux and aarch64] -- libxdamage-cos7-aarch64 # [linux and aarch64] -- libxext-cos7-aarch64 # [linux and aarch64] -- libxfixes-cos7-aarch64 # [linux and aarch64] -- libxi-cos7-aarch64 # [linux and aarch64] -- libxi-devel-cos7-aarch64 # [linux and aarch64] -- libxxf86vm-cos7-aarch64 # [linux and aarch64] -- mesa-dri-drivers-cos7-aarch64 # [linux and aarch64] -- mesa-khr-devel-cos7-aarch64 # [linux and aarch64] -- mesa-libegl-cos7-aarch64 # [linux and aarch64] -- mesa-libegl-devel-cos7-aarch64 # [linux and aarch64] -- mesa-libgbm-cos7-aarch64 # [linux and aarch64] -- mesa-libgl-cos7-aarch64 # [linux and aarch64] -- mesa-libgl-devel-cos7-aarch64 # [linux and aarch64] -- mesa-libglapi-cos7-aarch64 # [linux and aarch64] -- pixman-cos7-aarch64 # [linux and aarch64] -- sysroot_linux-aarch64 # [linux and aarch64] -- xorg-x11-server-common-cos7-aarch64 # [linux and aarch64] -- xorg-x11-server-xvfb-cos7-aarch64 # [linux and aarch64] -- sed # [unix] -- ccache -- clang-format -- cmake -- coin3d -- compilers -- conda -- conda-devenv -- debugpy -- doxygen -- eigen -- fmt -- freetype -- git -- gmsh -- graphviz -- hdf5 -- libboost-devel -- libcxx -- mamba -- matplotlib -- ninja -- numpy -- occt -- openssl -- pcl -- pip -- pivy -- pkg-config -- ply -- pre-commit -- pybind11 -- pyside2 -- python==3.11.* -- pyyaml -- qt-main -- smesh -- swig -- vtk==9.2.6 -- xerces-c -- yaml-cpp -- zlib -- zstd==1.5.6 diff --git a/conda/setup-environment-qt6.sh b/conda/setup-environment-qt6.sh deleted file mode 100755 index d656491fb3..0000000000 --- a/conda/setup-environment-qt6.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - -# create the conda environment as a subdirectory -mamba env create -p .conda/freecad -f conda/conda-env.yaml - -# add the environment subdirectory to the conda configuration -conda config --add envs_dirs $CONDA_PREFIX/envs -conda config --add envs_dirs $(pwd)/.conda -conda config --set env_prompt "({name})" - -# install the FreeCAD dependencies into the environment -mamba run --live-stream -n freecad mamba-devenv --no-prune -f conda/environment-qt6.devenv.yml diff --git a/conda/setup-environment.cmd b/conda/setup-environment.cmd deleted file mode 100644 index dd51d7c739..0000000000 --- a/conda/setup-environment.cmd +++ /dev/null @@ -1,10 +0,0 @@ -:: create the conda environment as a subdirectory -call mamba env create -p .conda/freecad -f conda/conda-env.yaml - -:: add the environment subdirectory to the conda configuration -call conda config --add envs_dirs %CONDA_PREFIX%/envs -call conda config --add envs_dirs %CD%/.conda -call conda config --set env_prompt "({name})" - -:: install the FreeCAD dependencies into the environment -call mamba run --live-stream -n freecad mamba-devenv --no-prune -f conda/environment.devenv.yml diff --git a/conda/setup-environment.sh b/conda/setup-environment.sh deleted file mode 100755 index bd75d76602..0000000000 --- a/conda/setup-environment.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - -# create the conda environment as a subdirectory -mamba env create -p .conda/freecad -f conda/conda-env.yaml - -# add the environment subdirectory to the conda configuration -conda config --add envs_dirs $CONDA_PREFIX/envs -conda config --add envs_dirs $(pwd)/.conda -conda config --set env_prompt "({name})" - -# install the FreeCAD dependencies into the environment -mamba run --live-stream -n freecad mamba-devenv --no-prune -f conda/environment.devenv.yml diff --git a/contrib/.vscode/launch.json b/contrib/.vscode/launch.json index c54dd2c8a0..b8c63d1c84 100644 --- a/contrib/.vscode/launch.json +++ b/contrib/.vscode/launch.json @@ -100,7 +100,7 @@ }, { "name": "Python debugger", - "type": "python", + "type": "debugpy", "request": "attach", "preLaunchTask": "WaitForDebugpy", "redirectOutput": true, @@ -131,4 +131,4 @@ } } ] -} \ No newline at end of file +} diff --git a/pixi.toml b/pixi.toml index f36510890c..a2f3377459 100644 --- a/pixi.toml +++ b/pixi.toml @@ -150,35 +150,41 @@ freecad-stubs = "*" ## Qt 6 Configuration Presets [target.linux-64.tasks] -configure = { cmd = [ "cmake", "-B", "build", "--preset", "conda-linux-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} -configure-debug = { cmd = [ "cmake", "-B", "build", "--preset", "conda-linux-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} -configure-release = { cmd = [ "cmake", "-B", "build", "--preset", "conda-linux-release", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} +configure-debug = { cmd = [ "cmake", "--preset", "conda-linux-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} +configure-release = { cmd = [ "cmake", "--preset", "conda-linux-release", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} [target.linux-aarch64.tasks] -configure = { cmd = [ "cmake", "-B", "build", "--preset", "conda-linux-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on= ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} -configure-debug = { cmd = [ "cmake", "-B", "build", "--preset", "conda-linux-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on= ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} -configure-release = { cmd = [ "cmake", "-B", "build", "--preset", "conda-linux-release", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on= ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} +configure-debug = { cmd = [ "cmake", "--preset", "conda-linux-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on= ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} +configure-release = { cmd = [ "cmake", "--preset", "conda-linux-release", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on= ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} [target.osx-64.tasks] -configure = { cmd = [ "cmake", "-B", "build", "--preset", "conda-macos-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} -configure-debug = { cmd = [ "cmake", "-B", "build", "--preset", "conda-macos-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} -configure-release = { cmd = [ "cmake", "-B", "build", "--preset", "conda-macos-release", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} +configure-debug = { cmd = [ "cmake", "--preset", "conda-macos-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} +configure-release = { cmd = [ "cmake", "--preset", "conda-macos-release", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} [target.osx-arm64.tasks] -configure = { cmd = [ "cmake", "-B", "build", "--preset", "conda-macos-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} -configure-debug = { cmd = [ "cmake", "-B", "build", "--preset", "conda-macos-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} -configure-release = { cmd = [ "cmake", "-B", "build", "--preset", "conda-macos-release", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} +configure-debug = { cmd = [ "cmake", "--preset", "conda-macos-debug", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} +configure-release = { cmd = [ "cmake", "--preset", "conda-macos-release", "-DBUILD_REVERSEENGINEERING=OFF" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} [target.win-64.tasks] -configure = { cmd = [ "cmake", "-B", "build", "--preset", "conda-windows-debug", "-DBUILD_REVERSEENGINEERING=OFF", "-DCMAKE_GENERATOR_PLATFORM=", "-DCMAKE_GENERATOR_TOOLSET=" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} -configure-debug = { cmd = [ "cmake", "-B", "build", "--preset", "conda-windows-debug", "-DBUILD_REVERSEENGINEERING=OFF", "-DCMAKE_GENERATOR_PLATFORM=", "-DCMAKE_GENERATOR_TOOLSET=" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} -configure-release = { cmd = [ "cmake", "-B", "build", "--preset", "conda-windows-release", "-DBUILD_REVERSEENGINEERING=OFF", "-DCMAKE_GENERATOR_PLATFORM=", "-DCMAKE_GENERATOR_TOOLSET=" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} -freecad = { cmd = [ ".pixi/envs/default/Library/bin/FreeCAD.exe" ], depends-on = ["install"]} +configure-debug = { cmd = [ "cmake", "--preset", "conda-windows-debug", "-DBUILD_REVERSEENGINEERING=OFF", "-DCMAKE_GENERATOR_PLATFORM=", "-DCMAKE_GENERATOR_TOOLSET=" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} +configure-release = { cmd = [ "cmake", "--preset", "conda-windows-release", "-DBUILD_REVERSEENGINEERING=OFF", "-DCMAKE_GENERATOR_PLATFORM=", "-DCMAKE_GENERATOR_TOOLSET=" ], depends-on = ["initialize"], env={ CFLAGS="", CXXFLAGS="", DEBUG_CFLAGS="", DEBUG_CXXFLAGS="" }} +freecad-debug = { cmd = [ ".pixi/envs/default/Library/bin/FreeCAD.exe" ], depends-on = ["install-debug"]} +freecad-release = { cmd = [ ".pixi/envs/default/Library/bin/FreeCAD.exe" ], depends-on = ["install-release"]} -## Tasks [tasks] initialize = { cmd = ["git", "submodule", "update", "--init", "--recursive"]} -build = { cmd = [ "cmake", "--build", "build" ] } -install = { cmd = [ "cmake", "--install", "build" ] } -test = { cmd = [ "ctest", "--test-dir", "build" ] } -freecad = "build/bin/FreeCAD" +# redirect to debug by default +configure = [{ task = "configure-debug" }] +build = [{ task = "build-debug" }] +install = [{ task = "install-debug" }] +test = [{ task = "test-debug" }] +freecad = [{ task = "freecad-debug" }] + +build-debug = { cmd = ["cmake", "--build", "build/debug"]} +build-release = { cmd = ["cmake", "--build", "build/release"]} +install-debug = { cmd = ["cmake", "--install", "build/debug"]} +install-release = { cmd = ["cmake", "--install", "build/release"]} +test-debug = { cmd = ["ctest", "--test-dir", "build/debug"]} +test-release = { cmd = ["ctest", "--test-dir", "build/release"]} +freecad-debug = "build/debug/bin/FreeCAD" +freecad-release = "build/release/bin/FreeCAD" diff --git a/src/Base/Tools.h b/src/Base/Tools.h index 88982c0ae6..404cdae6ba 100644 --- a/src/Base/Tools.h +++ b/src/Base/Tools.h @@ -356,6 +356,33 @@ struct BaseExport ZipTools }; +/** + * Helper struct to define inline overloads for the visitor pattern in std::visit. + * + * It uses type deduction to infer the type from the expression and creates a dedicated type that + * essentially is callable using any overload supplied. + * + * @code + * using Base::Overloads; + * + * const auto visitor = Overloads + * { + * [](int i){ std::print("int = {}\n", i); }, + * [](std::string_view s){ std::println("string = “{}”", s); }, + * [](const Base&){ std::println("base"); }, + * }; + * @endcode + * + * @see https://en.cppreference.com/w/cpp/utility/variant/visit + * + * @tparam Ts Types for functions that will be used for overloads + */ +template +struct Overloads: Ts... +{ + using Ts::operator()...; +}; + } // namespace Base #endif // SRC_BASE_TOOLS_H_ diff --git a/src/Gui/Application.cpp b/src/Gui/Application.cpp index 2f7466b13f..2cd30d8b8a 100644 --- a/src/Gui/Application.cpp +++ b/src/Gui/Application.cpp @@ -137,9 +137,11 @@ #include "WorkbenchManipulator.h" #include "WidgetFactory.h" #include "3Dconnexion/navlib/NavlibInterface.h" +#include "Inventor/SoFCPlacementIndicatorKit.h" #include "QtWidgets.h" -#include +#include +#include #ifdef BUILD_TRACY_FRAME_PROFILER #include @@ -208,6 +210,8 @@ struct ApplicationP // Create the Theme Manager prefPackManager = new PreferencePackManager(); + // Create the Style Parameter Manager + styleParameterManager = new StyleParameters::ParameterManager(); } ~ApplicationP() @@ -221,8 +225,11 @@ struct ApplicationP /// Active document Gui::Document* activeDocument {nullptr}; Gui::Document* editDocument {nullptr}; + MacroManager* macroMngr; PreferencePackManager* prefPackManager; + StyleParameters::ParameterManager* styleParameterManager; + /// List of all registered views std::list passive; bool isClosing {false}; @@ -372,6 +379,31 @@ struct PyMethodDef FreeCADGui_methods[] = { } // namespace Gui +void Application::initStyleParameterManager() +{ + Base::registerServiceImplementation( + new StyleParameters::BuiltInParameterSource({.name = QT_TR_NOOP("Built-in Parameters")})); + + Base::registerServiceImplementation( + new StyleParameters::UserParameterSource( + App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Themes/Tokens"), + {.name = QT_TR_NOOP("Theme Parameters"), + .options = StyleParameters::ParameterSourceOption::UserEditable})); + + Base::registerServiceImplementation( + new StyleParameters::UserParameterSource( + App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Themes/UserTokens"), + {.name = QT_TR_NOOP("User Parameters"), + .options = StyleParameters::ParameterSource::UserEditable})); + + for (auto* source : Base::provideServiceImplementations()) { + d->styleParameterManager->addSource(source); + } + + Base::registerServiceImplementation(d->styleParameterManager); +} // clang-format off Application::Application(bool GUIenabled) { @@ -576,6 +608,8 @@ Application::Application(bool GUIenabled) d = new ApplicationP(GUIenabled); + initStyleParameterManager(); + // global access Instance = this; @@ -1955,6 +1989,11 @@ Gui::PreferencePackManager* Application::prefPackManager() return d->prefPackManager; } +Gui::StyleParameters::ParameterManager* Application::styleParameterManager() +{ + return d->styleParameterManager; +} + //************************************************************************** // Init, Destruct and singleton @@ -2314,7 +2353,7 @@ void Application::runApplication() setenv("COIN_EGL", "1", 1); } #endif - + // Make sure that we use '.' as decimal point. See also // http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=559846 // and issue #0002891 @@ -2498,36 +2537,22 @@ void Application::setStyleSheet(const QString& qssFile, bool tiledBackground) } } -QString Application::replaceVariablesInQss(QString qssText) +void Application::reloadStyleSheet() { - // First we fetch the colors from preferences, - ParameterGrp::handle hGrp = - App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Themes"); - unsigned long longAccentColor1 = hGrp->GetUnsigned("ThemeAccentColor1", 0); - unsigned long longAccentColor2 = hGrp->GetUnsigned("ThemeAccentColor2", 0); - unsigned long longAccentColor3 = hGrp->GetUnsigned("ThemeAccentColor3", 0); + const MainWindow* mw = getMainWindow(); - // convert them to hex. - // Note: the ulong contains alpha channels so 8 hex characters when we need 6 here. - QString accentColor1 = QStringLiteral("#%1") - .arg(longAccentColor1, 8, 16, QLatin1Char('0')) - .toUpper() - .mid(0, 7); - QString accentColor2 = QStringLiteral("#%1") - .arg(longAccentColor2, 8, 16, QLatin1Char('0')) - .toUpper() - .mid(0, 7); - QString accentColor3 = QStringLiteral("#%1") - .arg(longAccentColor3, 8, 16, QLatin1Char('0')) - .toUpper() - .mid(0, 7); + const QString qssFile = mw->property("fc_currentStyleSheet").toString(); + const bool tiledBackground = mw->property("fc_tiledBackground").toBool(); - qssText = qssText.replace(QStringLiteral("@ThemeAccentColor1"), accentColor1); - qssText = qssText.replace(QStringLiteral("@ThemeAccentColor2"), accentColor2); - qssText = qssText.replace(QStringLiteral("@ThemeAccentColor3"), accentColor3); + d->styleParameterManager->reload(); - // Base::Console().warning("%s\n", qssText.toStdString()); - return qssText; + setStyleSheet(qssFile, tiledBackground); + OverlayManager::instance()->refresh(nullptr, true); +} + +QString Application::replaceVariablesInQss(const QString& qssText) +{ + return QString::fromStdString(d->styleParameterManager->replacePlaceholders(qssText.toStdString())); } void Application::checkForDeprecatedSettings() diff --git a/src/Gui/Application.h b/src/Gui/Application.h index 596fc32dae..45af443579 100644 --- a/src/Gui/Application.h +++ b/src/Gui/Application.h @@ -30,6 +30,8 @@ #include +#include "StyleParameters/ParameterManager.h" + class QCloseEvent; class SoNode; class NavlibInterface; @@ -63,6 +65,9 @@ public: /// destruction ~Application(); + /// Initializes default configuration for Style Parameter Manager + void initStyleParameterManager(); + /** @name methods for support of files */ //@{ /// open a file @@ -221,7 +226,8 @@ public: //@{ /// Activate a stylesheet void setStyleSheet(const QString& qssFile, bool tiledBackground); - QString replaceVariablesInQss(QString qssText); + void reloadStyleSheet(); + QString replaceVariablesInQss(const QString& qssText); //@} /** @name User Commands */ @@ -235,6 +241,7 @@ public: //@} Gui::PreferencePackManager* prefPackManager(); + Gui::StyleParameters::ParameterManager* styleParameterManager(); /** @name Init, Destruct an Access methods */ //@{ diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt index ab93d16cac..e4979d9d51 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -55,6 +55,7 @@ include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/Quarter ${CMAKE_CURRENT_SOURCE_DIR}/PreferencePages ${CMAKE_CURRENT_SOURCE_DIR}/Selection + ${CMAKE_CURRENT_SOURCE_DIR}/StyleParameters ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/.. ${CMAKE_CURRENT_BINARY_DIR}/.. @@ -401,6 +402,7 @@ SET(Gui_UIC_SRCS Dialogs/DlgProjectUtility.ui Dialogs/DlgPropertyLink.ui Dialogs/DlgRevertToBackupConfig.ui + Dialogs/DlgThemeEditor.ui PreferencePages/DlgSettings3DView.ui PreferencePages/DlgSettingsCacheDirectory.ui Dialogs/DlgSettingsColorGradient.ui @@ -516,6 +518,7 @@ SET(Dialog_CPP_SRCS Dialogs/DlgPropertyLink.cpp Dialogs/DlgRevertToBackupConfigImp.cpp Dialogs/DlgExpressionInput.cpp + Dialogs/DlgThemeEditor.cpp TaskDlgRelocation.cpp Dialogs/DlgCheckableMessageBox.cpp TaskTransform.cpp @@ -558,6 +561,7 @@ SET(Dialog_HPP_SRCS Dialogs/DlgRevertToBackupConfigImp.h Dialogs/DlgCheckableMessageBox.h Dialogs/DlgExpressionInput.h + Dialogs/DlgThemeEditor.h TaskDlgRelocation.h TaskTransform.h Dialogs/DlgUndoRedo.h @@ -1326,6 +1330,8 @@ SET(FreeCADGui_CPP_SRCS StartupProcess.cpp TransactionObject.cpp ToolHandler.cpp + StyleParameters/Parser.cpp + StyleParameters/ParameterManager.cpp ) SET(FreeCADGui_SRCS Application.h @@ -1368,6 +1374,8 @@ SET(FreeCADGui_SRCS StartupProcess.h TransactionObject.h ToolHandler.h + StyleParameters/Parser.h + StyleParameters/ParameterManager.h ) SET(FreeCADGui_SRCS diff --git a/src/Gui/CommandStd.cpp b/src/Gui/CommandStd.cpp index 2837ccd190..cc93595e84 100644 --- a/src/Gui/CommandStd.cpp +++ b/src/Gui/CommandStd.cpp @@ -574,8 +574,8 @@ StdCmdFreeCADDonation::StdCmdFreeCADDonation() :Command("Std_FreeCADDonation") { sGroup = "Help"; - sMenuText = QT_TR_NOOP("Support FreeCA&D"); - sToolTipText = QT_TR_NOOP("Support FreeCAD development"); + sMenuText = QT_TR_NOOP("Donate to FreeCA&D"); + sToolTipText = QT_TR_NOOP("Support the FreeCAD development"); sWhatsThis = "Std_FreeCADDonation"; sStatusTip = sToolTipText; sPixmap = "internet-web-browser"; @@ -586,11 +586,45 @@ void StdCmdFreeCADDonation::activated(int iMsg) { Q_UNUSED(iMsg); ParameterGrp::handle hURLGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Websites"); - std::string url = hURLGrp->GetASCII("DonatePage", "https://wiki.freecad.org/Donate"); + std::string url = hURLGrp->GetASCII("DonatePage", "https://www.freecad.org/sponsor"); hURLGrp->SetASCII("DonatePage", url.c_str()); OpenURLInBrowser(url.c_str()); } +//=========================================================================== +// Std_FreeDevHandbook + +//=========================================================================== + +DEF_STD_CMD(StdCmdDevHandbook) + +StdCmdDevHandbook::StdCmdDevHandbook() + + : Command("Std_DevHandbook") +{ + sGroup = "Help"; + sMenuText = QT_TR_NOOP("Developers Handbook"); + + sToolTipText = QT_TR_NOOP("Handbook about FreeCAD development"); + + sWhatsThis = "Std_DevHandbook"; + sStatusTip = sToolTipText; + sPixmap = "internet-web-browser"; + eType = 0; +} + +void StdCmdDevHandbook::activated(int iMsg) + +{ + Q_UNUSED(iMsg); + ParameterGrp::handle hURLGrp = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/Websites"); + std::string url = hURLGrp->GetASCII("DevHandbook", "https://freecad.github.io/DevelopersHandbook/"); + + hURLGrp->SetASCII("DevHandbook", url.c_str()); + OpenURLInBrowser(url.c_str()); +} + //=========================================================================== // Std_FreeCADWebsite //=========================================================================== @@ -647,34 +681,6 @@ void StdCmdFreeCADUserHub::activated(int iMsg) OpenURLInBrowser(url.c_str()); } -//=========================================================================== -// Std_FreeCADPowerUserHub -//=========================================================================== - -DEF_STD_CMD(StdCmdFreeCADPowerUserHub) - -StdCmdFreeCADPowerUserHub::StdCmdFreeCADPowerUserHub() - :Command("Std_FreeCADPowerUserHub") -{ - sGroup = "Help"; - sMenuText = QT_TR_NOOP("&Python Scripting Documentation"); - sToolTipText = QT_TR_NOOP("Opens the Python Scripting documentation"); - sWhatsThis = "Std_FreeCADPowerUserHub"; - sStatusTip = sToolTipText; - sPixmap = "applications-python"; - eType = 0; -} - -void StdCmdFreeCADPowerUserHub::activated(int iMsg) -{ - Q_UNUSED(iMsg); - std::string defaulturl = QCoreApplication::translate(this->className(),"https://wiki.freecad.org/Power_users_hub").toStdString(); - ParameterGrp::handle hURLGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Websites"); - std::string url = hURLGrp->GetASCII("PowerUsers", defaulturl.c_str()); - hURLGrp->SetASCII("PowerUsers", url.c_str()); - OpenURLInBrowser(url.c_str()); -} - //=========================================================================== // Std_FreeCADForum //=========================================================================== @@ -703,59 +709,6 @@ void StdCmdFreeCADForum::activated(int iMsg) OpenURLInBrowser(url.c_str()); } -//=========================================================================== -// Std_FreeCADFAQ -//=========================================================================== - -DEF_STD_CMD(StdCmdFreeCADFAQ) - -StdCmdFreeCADFAQ::StdCmdFreeCADFAQ() - :Command("Std_FreeCADFAQ") -{ - sGroup = "Help"; - sMenuText = QT_TR_NOOP("FreeCAD FA&Q"); - sToolTipText = QT_TR_NOOP("Opens the Frequently Asked Questions"); - sWhatsThis = "Std_FreeCADFAQ"; - sStatusTip = sToolTipText; - sPixmap = "internet-web-browser"; - eType = 0; -} - -void StdCmdFreeCADFAQ::activated(int iMsg) -{ - Q_UNUSED(iMsg); - std::string defaulturl = QCoreApplication::translate(this->className(),"https://wiki.freecad.org/Frequently_asked_questions").toStdString(); - ParameterGrp::handle hURLGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Websites"); - std::string url = hURLGrp->GetASCII("FAQ", defaulturl.c_str()); - hURLGrp->SetASCII("FAQ", url.c_str()); - OpenURLInBrowser(url.c_str()); -} - -//=========================================================================== -// Std_PythonWebsite -//=========================================================================== - -DEF_STD_CMD(StdCmdPythonWebsite) - -StdCmdPythonWebsite::StdCmdPythonWebsite() - :Command("Std_PythonWebsite") -{ - sGroup = "Help"; - sMenuText = QT_TR_NOOP("Python Website"); - sToolTipText = QT_TR_NOOP("The official Python website"); - sWhatsThis = "Std_PythonWebsite"; - sStatusTip = QT_TR_NOOP("Python Website"); - sPixmap = "applications-python"; - eType = 0; -} - -void StdCmdPythonWebsite::activated(int iMsg) -{ - Q_UNUSED(iMsg); - OpenURLInBrowser("https://www.python.org"); -} - - //=========================================================================== // Std_ReportBug //=========================================================================== @@ -964,12 +917,7 @@ StdCmdReloadStyleSheet::StdCmdReloadStyleSheet() void StdCmdReloadStyleSheet::activated(int ) { - auto mw = getMainWindow(); - - auto qssFile = mw->property("fc_currentStyleSheet").toString(); - auto tiledBackground = mw->property("fc_tiledBackground").toBool(); - - Gui::Application::Instance->setStyleSheet(qssFile, tiledBackground); + Application::Instance->reloadStyleSheet(); } namespace Gui { @@ -996,15 +944,13 @@ void CreateStdCommands() rcCmdMgr.addCommand(new StdCmdFreeCADWebsite()); rcCmdMgr.addCommand(new StdCmdFreeCADDonation()); rcCmdMgr.addCommand(new StdCmdFreeCADUserHub()); - rcCmdMgr.addCommand(new StdCmdFreeCADPowerUserHub()); rcCmdMgr.addCommand(new StdCmdFreeCADForum()); - rcCmdMgr.addCommand(new StdCmdFreeCADFAQ()); - rcCmdMgr.addCommand(new StdCmdPythonWebsite()); rcCmdMgr.addCommand(new StdCmdReportBug()); rcCmdMgr.addCommand(new StdCmdTextDocument()); rcCmdMgr.addCommand(new StdCmdUnitsCalculator()); rcCmdMgr.addCommand(new StdCmdUserEditMode()); rcCmdMgr.addCommand(new StdCmdReloadStyleSheet()); + rcCmdMgr.addCommand(new StdCmdDevHandbook()); //rcCmdMgr.addCommand(new StdCmdDownloadOnlineHelp()); //rcCmdMgr.addCommand(new StdCmdDescription()); } diff --git a/src/Gui/Dialogs/DlgThemeEditor.cpp b/src/Gui/Dialogs/DlgThemeEditor.cpp new file mode 100644 index 0000000000..eeabf70581 --- /dev/null +++ b/src/Gui/Dialogs/DlgThemeEditor.cpp @@ -0,0 +1,764 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kacper Donat * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD 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. * + * * + * FreeCAD 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 FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#include "Tools.h" +#include "Application.h" +#include "OverlayManager.h" +#include "DlgThemeEditor.h" +#include "ui_DlgThemeEditor.h" +#include "BitmapFactory.h" + +#include +#include + +#ifndef _PreComp_ +# include +# include +# include +# include +# include +#endif + +QPixmap colorPreview(const QColor& color) +{ + constexpr qsizetype size = 16; + + QPixmap preview = Gui::BitmapFactory().empty({ size, size }); + + QPainter painter(&preview); + painter.setRenderHint(QPainter::Antialiasing); + painter.setPen(Qt::NoPen); + painter.setBrush(color); + painter.drawEllipse(QRect { 0, 0, size, size }); + + return preview; +} + +QString typeOfTokenValue(const Gui::StyleParameters::Value& value) +{ + // clang-format off + return std::visit( + Base::Overloads { + [](const std::string&) { + return QWidget::tr("Generic"); + }, + [](const Gui::StyleParameters::Length&) { + return QWidget::tr("Length"); + }, + [](const QColor&) { + return QWidget::tr("Color"); + } + }, + value + ); + // clang-format on +} + +namespace Gui +{ +struct StyleParametersModel::Item +{ + Item() = default; + virtual ~Item() = default; + + FC_DEFAULT_COPY_MOVE(Item); + + virtual bool isHeader() const = 0; +}; + +struct StyleParametersModel::GroupItem: Item +{ + explicit GroupItem(QString title, ParameterSource* source) + : title(std::move(title)) + , canAddNewParameters(source && source->metadata.options.testFlag(UserEditable)) + , source(source) + {} + + bool isHeader() const override + { + return true; + } + + QString title; + bool canAddNewParameters {false}; + ParameterSource* source; + std::set deleted {}; +}; + +struct StyleParametersModel::ParameterItem: Item +{ + ParameterItem(QString name, StyleParameters::Parameter token) + : name(std::move(name)) + , token(std::move(token)) + {} + + bool isHeader() const override + { + return false; + } + + QString name; + StyleParameters::Parameter token; + QFlags flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable; +}; + +class StyleParametersModel::Node +{ +public: + explicit Node(std::unique_ptr data, Node* parent = nullptr) + : _parent(parent) + , _data(std::move(data)) + {} + + void appendChild(std::unique_ptr child) + { + child->_parent = this; + _children.push_back(std::move(child)); + } + + void removeChild(const int row) + { + if (row >= 0 && row < static_cast(_children.size())) { + _children.erase(_children.begin() + row); + } + } + + Node* child(const int row) const + { + if (row < 0 || row >= static_cast(_children.size())) { + if (!_empty) { + _empty = std::make_unique(nullptr, const_cast(this)); + } + + return _empty.get(); + } + + return _children[row].get(); + } + + int childCount() const + { + return static_cast(_children.size()); + } + + int row() const + { + if (!_parent) { + return 0; + } + + const auto& siblings = _parent->_children; + for (size_t i = 0; i < siblings.size(); ++i) { + if (siblings[i].get() == this) { + return static_cast(i); + } + } + + return -1; + } + + Item* data() const + { + return _data.get(); + } + + template + T* data() const + { + return dynamic_cast(_data.get()); + } + + Node* parent() const + { + return _parent; + } + +private: + Node* _parent; + std::vector> _children {}; + + mutable std::unique_ptr _empty {}; + std::unique_ptr _data {}; +}; + +class DlgThemeEditor::Delegate: public QStyledItemDelegate +{ + Q_OBJECT + + QRegularExpression validNameRegExp { QStringLiteral("^[A-Z][a-zA-Z0-9]*$") }; + QRegularExpressionValidator* nameValidator; + +public: + explicit Delegate(QObject* parent = nullptr) + : QStyledItemDelegate(parent) + , nameValidator(new QRegularExpressionValidator(validNameRegExp, this)) + {} + + QWidget* createEditor(QWidget* parent, + [[maybe_unused]] const QStyleOptionViewItem& option, + const QModelIndex& index) const override + { + auto model = dynamic_cast(index.model()); + if (!model) { + return nullptr; + } + + if (model->item(index) + && index.column() == StyleParametersModel::ParameterExpression) { + return new QLineEdit(parent); + } + + if (index.column() == StyleParametersModel::ParameterName) { + auto editor = new QLineEdit(parent); + editor->setValidator(nameValidator); + return editor; + } + + return nullptr; + } + + void setEditorData(QWidget* editor, const QModelIndex& index) const override + { + if (auto* lineEdit = qobject_cast(editor)) { + lineEdit->setText(index.data(Qt::DisplayRole).toString()); + } + } + + void setModelData(QWidget* editor, + QAbstractItemModel* model, + const QModelIndex& index) const override + { + if (auto* lineEdit = qobject_cast(editor)) { + model->setData(index, lineEdit->text(), Qt::EditRole); + } + } + + void updateEditorGeometry(QWidget* editor, + const QStyleOptionViewItem& option, + [[maybe_unused]] const QModelIndex& index) const override + { + editor->setGeometry(option.rect); + } + + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override + { + constexpr int height = 36; + + QSize base = QStyledItemDelegate::sizeHint(option, index); + return {base.width(), std::max(base.height(), height)}; + } + + void paintAddPlaceholder(QPainter* painter, const QStyleOptionViewItem& option) const + { + QStyle* style = option.widget ? option.widget->style() : QApplication::style(); + QRect rect = style->subElementRect(QStyle::SE_ItemViewItemText, &option, option.widget); + + QFont font = option.font; + font.setItalic(true); + + painter->setFont(font); + painter->drawText(rect, Qt::AlignLeft | Qt::AlignVCenter, tr("New parameter...")); + } + + void paint(QPainter* painter, + const QStyleOptionViewItem& option, + const QModelIndex& index) const override + { + auto model = dynamic_cast(index.model()); + + painter->save(); + + QStyleOptionViewItem opt(option); + initStyleOption(&opt, index); + + if (model->isAddPlaceholder(index)) { + if (index.column() == StyleParametersModel::ParameterName) { + paintAddPlaceholder(painter, opt); + } + } + else if (model->item(index)) { + constexpr int headerContrast = 120; + + const bool isLightTheme = option.palette.color(QPalette::Text).lightness() < 128; + + const QColor headerBackgroundColor = QtTools::valueOr( + option.widget->property("headerBackgroundColor"), + isLightTheme + ? option.palette.color(QPalette::AlternateBase).darker(headerContrast) + : option.palette.color(QPalette::AlternateBase).lighter(headerContrast) + ); + + painter->fillRect(option.rect, headerBackgroundColor); + QStyledItemDelegate::paint(painter, option, index); + } + else { + QStyledItemDelegate::paint(painter, option, index); + } + + painter->restore(); + } +}; + +void TokenTreeView::keyPressEvent(QKeyEvent* event) +{ + static constexpr auto expressionEditKeys = { Qt::Key_Return, Qt::Key_Enter, Qt::Key_Space }; + static constexpr auto nameEditKeys = { Qt::Key_F2 }; + static constexpr auto deleteKeys = { Qt::Key_Delete }; + + const auto isCorrectKey = [&event](auto key) { return event->key() == key; }; + + if (QModelIndex index = currentIndex(); index.isValid()) { + if (std::ranges::any_of(expressionEditKeys, isCorrectKey)) { + edit(index.siblingAtColumn(StyleParametersModel::ParameterExpression)); + return; + } + + if (std::ranges::any_of(nameEditKeys, isCorrectKey)) { + edit(index.siblingAtColumn(StyleParametersModel::ParameterName)); + return; + } + + if (std::ranges::any_of(deleteKeys, isCorrectKey)) { + requestRemove(currentIndex()); + return; + } + } + + QTreeView::keyPressEvent(event); +} + +StyleParametersModel::StyleParametersModel( + const std::list& sources, + QObject* parent) + : QAbstractItemModel(parent) + , ParameterSource({ .name = QT_TR_NOOP("All Theme Editor Parameters") }) + , sources(sources) + , manager(new StyleParameters::ParameterManager()) +{ + // The parameter model serves as the source, so the manager can compute all necessary things + manager->addSource(this); + + reset(); +} + +StyleParametersModel::~StyleParametersModel() = default; + +std::list StyleParametersModel::all() const +{ + std::map result; + + QtTools::walkTreeModel(this, [this, &result](const QModelIndex& index) { + if (auto parameterItem = item(index)) { + if (result.contains(parameterItem->token.name)) { + return; + } + + result[parameterItem->token.name] = parameterItem->token; + } + }); + + const auto values = result | std::ranges::views::values; + return std::list(values.begin(), values.end()); +} + +std::optional StyleParametersModel::get(const std::string& name) const +{ + std::optional result = std::nullopt; + + QtTools::walkTreeModel(this, [this, &name, &result](const QModelIndex& index) { + if (auto parameterItem = item(index)) { + if (parameterItem->token.name == name) { + result = parameterItem->token; + return true; + } + } + + return false; + }); + + return result; +} + +void StyleParametersModel::removeItem(const QModelIndex& index) +{ + if (auto parameterItem = item(index)) { + auto groupItem = item(index.parent()); + + if (!groupItem->source->metadata.options.testFlag(UserEditable)) { + return; + } + + groupItem->deleted.insert(parameterItem->token.name); + + beginRemoveRows(index.parent(), index.row(), index.row()); + node(index.parent())->removeChild(index.row()); + endRemoveRows(); + } +} + +void StyleParametersModel::reset() +{ + using enum StyleParameters::ParameterSourceOption; + + beginResetModel(); + root = std::make_unique(std::make_unique(tr("Root"), nullptr)); + + for (auto* source : sources) { + auto groupNode = std::make_unique( + std::make_unique(tr(source->metadata.name.c_str()), source)); + + for (const auto& parameter : source->all()) { + auto item = std::make_unique( + std::make_unique(QString::fromStdString(parameter.name), parameter)); + + if (source->metadata.options.testFlag(ReadOnly)) { + item->data()->flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable; + } + + groupNode->appendChild(std::move(item)); + } + + root->appendChild(std::move(groupNode)); + } + + endResetModel(); +} + +void StyleParametersModel::flush() +{ + QtTools::walkTreeModel(this, [this](const QModelIndex& index) { + if (const auto& groupItem = item(index)) { + for (const auto& parameter : groupItem->deleted) { + groupItem->source->remove(parameter); + } + + groupItem->deleted.clear(); + } + + if (const auto& parameterItem = item(index)) { + const auto& groupItem = item(index.parent()); + + groupItem->source->define(parameterItem->token); + } + }); + + reset(); +} + +int StyleParametersModel::rowCount(const QModelIndex& index) const +{ + if (index.column() > 0) { + return 0; + } + + int childCount = node(index)->childCount(); + + if (const auto& groupItem = item(index)) { + return childCount + (groupItem->canAddNewParameters ? 1 : 0); + } + + return childCount; +} + +int StyleParametersModel::columnCount([[maybe_unused]] const QModelIndex& index) const +{ + return ColumnCount; +} + +QVariant StyleParametersModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { + switch (section) { + case ParameterName: + return tr("Name"); + case ParameterExpression: + return tr("Expression"); + case ParameterPreview: + return tr("Preview"); + case ParameterType: + return tr("Type"); + default: + return {}; + } + } + + return {}; +} + +QVariant StyleParametersModel::data(const QModelIndex& index, int role) const +{ + if (auto parameterItem = item(index)) { + const auto& [name, token, _] = *parameterItem; + const auto& value = manager->resolve(name.toStdString()); + + if (role == Qt::DisplayRole) { + if (index.column() == ParameterName) { + return name; + } + if (index.column() == ParameterExpression) { + return QString::fromStdString(token.value); + } + if (index.column() == ParameterType) { + return typeOfTokenValue(value); + } + if (index.column() == ParameterPreview) { + return QString::fromStdString(value.toString()); + } + } + + if (role == Qt::DecorationRole) { + if (index.column() == ParameterPreview && std::holds_alternative(value)) { + return colorPreview(std::get(value)); + } + } + } + + if (auto groupItem = item(index)) { + if (role == Qt::DisplayRole && index.column() == ParameterName) { + return groupItem->title; + } + } + + return {}; +} + +bool StyleParametersModel::setData(const QModelIndex& index, + const QVariant& value, + [[maybe_unused]] int role) +{ + if (auto parameterItem = item(index)) { + auto groupItem = item(index.parent()); + + if (index.column() == ParameterName) { + QString newName = value.toString(); + + StyleParameters::Parameter newToken = parameterItem->token; + newToken.name = newName.toStdString(); + + // there is no rename operation, so we need to mark the previous token as deleted + groupItem->deleted.insert(parameterItem->token.name); + + parameterItem->name = newName; + parameterItem->token = newToken; + } + + if (index.column() == ParameterExpression) { + QString newValue = value.toString(); + + StyleParameters::Parameter newToken = parameterItem->token; + newToken.value = newValue.toStdString(); + + parameterItem->token = newToken; + } + } + + if (isAddPlaceholder(index)) { + if (index.column() == ParameterName) { + QString newName = value.toString(); + + if (newName.isEmpty()) { + return false; + } + + StyleParameters::Parameter token { .name = newName.toStdString(), .value = "" }; + + int start = rowCount(index.parent()); + + beginInsertRows(index.parent(), start, start + 1); + auto item = std::make_unique( + std::make_unique(newName, token)); + node(index.parent())->appendChild(std::move(item)); + endInsertRows(); + + // this must be queued to basically next frame so widget has a chance to update + QTimer::singleShot(0, [this, index]() { + this->newParameterAdded(index); + }); + } + } + + this->manager->reload(); + + QtTools::walkTreeModel(this, [this](const QModelIndex& index) { + const QModelIndex previewColumnIndex = index.siblingAtColumn(ParameterPreview); + + Q_EMIT dataChanged(previewColumnIndex, previewColumnIndex); + }); + + return true; +} + +Qt::ItemFlags StyleParametersModel::flags(const QModelIndex& index) const +{ + if (auto parameterItem = item(index)) { + if (index.column() == ParameterName || index.column() == ParameterExpression) { + return parameterItem->flags | QAbstractItemModel::flags(index); + } + } + + if (isAddPlaceholder(index)) { + if (index.column() == ParameterName) { + return Qt::ItemIsEnabled | Qt::ItemIsEditable | QAbstractItemModel::flags(index); + } + } + + return QAbstractItemModel::flags(index); +} + +QModelIndex StyleParametersModel::index(int row, int col, const QModelIndex& parent) const +{ + if (!hasIndex(row, col, parent)) { + return {}; + } + + if (auto child = node(parent)->child(row)) { + return createIndex(row, col, child); + } + + return {}; +} + +QModelIndex StyleParametersModel::parent(const QModelIndex& index) const +{ + if (!index.isValid()) { + return {}; + } + + auto node = static_cast(index.internalPointer()); + auto parent = node->parent(); + + if (!parent || parent == root.get()) { + return {}; + } + + return createIndex(parent->row(), 0, parent); +} + +bool StyleParametersModel::isAddPlaceholder(const QModelIndex& index) const +{ + return item(index) == nullptr; +} + +StyleParametersModel::Node* StyleParametersModel::node(const QModelIndex& index) const +{ + return index.isValid() ? static_cast(index.internalPointer()) : root.get(); +} + +StyleParametersModel::Item* StyleParametersModel::item(const QModelIndex& index) const +{ + return node(index)->data(); +} + +DlgThemeEditor::DlgThemeEditor(QWidget* parent) + : QDialog(parent) + , ui(new Ui::DlgThemeEditor) + , model(std::make_unique( + Base::provideServiceImplementations(), + this)) +{ + ui->setupUi(this); + + ui->tokensTreeView->setMouseTracking(true); + ui->tokensTreeView->setItemDelegate(new Delegate(ui->tokensTreeView)); + ui->tokensTreeView->setModel(model.get()); + + constexpr int typeColumnWidth = 80; + constexpr int nameColumnWidth = 200; + + struct ColumnDefinition // NOLINT(*-pro-type-member-init) + { + StyleParametersModel::Column column; + QHeaderView::ResizeMode mode; + qsizetype defaultWidth = 0; + }; + + static constexpr std::initializer_list columnSizingDefinitions = { + {StyleParametersModel::ParameterName, QHeaderView::ResizeMode::ResizeToContents}, + {StyleParametersModel::ParameterExpression, QHeaderView::ResizeMode::Stretch}, + {StyleParametersModel::ParameterPreview, QHeaderView::ResizeMode::Stretch}, + {StyleParametersModel::ParameterType, QHeaderView::ResizeMode::Fixed, typeColumnWidth}, + }; + + for (const auto& [column, mode, defaultWidth] : columnSizingDefinitions) { + ui->tokensTreeView->header()->setSectionResizeMode(column, mode); + + if (defaultWidth > 0) { + ui->tokensTreeView->header()->setDefaultSectionSize(defaultWidth); + } + } + + ui->tokensTreeView->setColumnWidth(StyleParametersModel::ParameterName, nameColumnWidth); + ui->tokensTreeView->expandAll(); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + + connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &DlgThemeEditor::handleButtonClick); + + connect(ui->tokensTreeView, + &TokenTreeView::requestRemove, + model.get(), + qOverload(&StyleParametersModel::removeItem)); + + connect(model.get(), &StyleParametersModel::modelReset, ui->tokensTreeView, [this] { + ui->tokensTreeView->expandAll(); + }); + connect(model.get(), + &StyleParametersModel::newParameterAdded, + this, + [this](const QModelIndex& index) { + const auto newParameterExpressionIndex = + index.siblingAtColumn(StyleParametersModel::ParameterExpression); + + ui->tokensTreeView->scrollTo(newParameterExpressionIndex); + ui->tokensTreeView->setCurrentIndex(newParameterExpressionIndex); + ui->tokensTreeView->edit(newParameterExpressionIndex); + }); +} + +DlgThemeEditor::~DlgThemeEditor() = default; + +void DlgThemeEditor::handleButtonClick(QAbstractButton* button) +{ + auto role = ui->buttonBox->buttonRole(button); + + switch (role) { + case QDialogButtonBox::ApplyRole: + case QDialogButtonBox::AcceptRole: + model->flush(); + Application::Instance->reloadStyleSheet(); + break; + case QDialogButtonBox::ResetRole: + model->reset(); + break; + default: + // no-op + break; + } +} + +} // namespace Gui + +#include "DlgThemeEditor.moc" diff --git a/src/Gui/Dialogs/DlgThemeEditor.h b/src/Gui/Dialogs/DlgThemeEditor.h new file mode 100644 index 0000000000..6964742c6f --- /dev/null +++ b/src/Gui/Dialogs/DlgThemeEditor.h @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kacper Donat * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD 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. * + * * + * FreeCAD 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 FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#ifndef DLGTHEMEEDITOR_H +#define DLGTHEMEEDITOR_H + + +#include "StyleParameters/ParameterManager.h" + +#include +#include +#include + +QT_BEGIN_NAMESPACE +class QAbstractButton; +QT_END_NAMESPACE + +namespace Gui { + +namespace StyleParameters +{ +class ParameterManager; +} + +QT_BEGIN_NAMESPACE +namespace Ui { class DlgThemeEditor; } +QT_END_NAMESPACE + +class GuiExport TokenTreeView : public QTreeView +{ + Q_OBJECT +public: + using QTreeView::QTreeView; + +protected: + void keyPressEvent(QKeyEvent* event) override; + +Q_SIGNALS: + void requestRemove(const QModelIndex& index); +}; + +class GuiExport StyleParametersModel: public QAbstractItemModel, public StyleParameters::ParameterSource +{ + Q_OBJECT + + class Node; + +public: + struct Item; + struct GroupItem; + struct ParameterItem; + + enum Column : std::uint8_t + { + ParameterName, + ParameterExpression, + ParameterType, + ParameterPreview, + ColumnCount + }; + + FC_DISABLE_COPY_MOVE(StyleParametersModel); + + explicit StyleParametersModel(const std::list& sources, + QObject* parent = nullptr); + + ~StyleParametersModel() override; + + std::list all() const override; + std::optional get(const std::string& name) const override; + + void reset(); + void flush(); + + int rowCount(const QModelIndex& index) const override; + int columnCount(const QModelIndex& index) const override; + + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + QVariant data(const QModelIndex& index, int role) const override; + bool setData(const QModelIndex& index, const QVariant& value, int role) override; + + Qt::ItemFlags flags(const QModelIndex& index) const override; + + QModelIndex index(int row, int col, const QModelIndex& parent) const override; + QModelIndex parent(const QModelIndex& index) const override; + + Node* node(const QModelIndex& index) const; + Item* item(const QModelIndex& index) const; + + template + T* item(const QModelIndex& index) const + { + return dynamic_cast(item(index)); + } + + bool isAddPlaceholder(const QModelIndex& index) const; + +public Q_SLOTS: + void removeItem(const QModelIndex& index); + +Q_SIGNALS: + void newParameterAdded(const QModelIndex& index); + +private: + std::list sources; + std::unique_ptr manager; + std::unique_ptr root; +}; + +class GuiExport DlgThemeEditor : public QDialog { + Q_OBJECT + + class Delegate; + +public: + FC_DISABLE_COPY_MOVE(DlgThemeEditor); + + explicit DlgThemeEditor(QWidget *parent = nullptr); + + ~DlgThemeEditor() override; + +public Q_SLOTS: + void handleButtonClick(QAbstractButton* button); + +private: + std::unique_ptr ui; + std::unique_ptr manager; + std::unique_ptr model; +}; +} // Gui + +#endif //DLGTHEMEEDITOR_H diff --git a/src/Gui/Dialogs/DlgThemeEditor.ui b/src/Gui/Dialogs/DlgThemeEditor.ui new file mode 100644 index 0000000000..dfc7802cf0 --- /dev/null +++ b/src/Gui/Dialogs/DlgThemeEditor.ui @@ -0,0 +1,156 @@ + + + Gui::DlgThemeEditor + + + + 0 + 0 + 1169 + 562 + + + + Theme Editor + + + + + + Preview + + + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter + + + + + + CheckBox + + + + + + + Qt::Orientation::Horizontal + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + RadioButton + + + + + + + + Item 1 + + + + + Item 2 + + + + + + + + PushButton + + + + + + + + Tab 1 + + + + + Tab 2 + + + + + + + + + + + + + + + + QDialogButtonBox::StandardButton::Apply|QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok|QDialogButtonBox::StandardButton::Reset + + + + + + + + + true + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + QAbstractItemView::EditTrigger::DoubleClicked + + + true + + + QAbstractItemView::SelectionBehavior::SelectRows + + + false + + + true + + + + + + + + Gui::ColorButton + QPushButton +
Gui/Widgets.h
+
+ + Gui::TokenTreeView + QTreeView +
Gui/Dialogs/DlgThemeEditor.h
+
+
+ + +
diff --git a/src/Gui/OverlayManager.cpp b/src/Gui/OverlayManager.cpp index 8c1cb144e7..15fdc211c1 100644 --- a/src/Gui/OverlayManager.cpp +++ b/src/Gui/OverlayManager.cpp @@ -133,6 +133,8 @@ public: if (activeStyleSheet.isEmpty()) { activeStyleSheet = _default; } + + activeStyleSheet = Application::Instance->replaceVariablesInQss(activeStyleSheet); } ParameterGrp::handle handle; @@ -157,7 +159,7 @@ private: } else if (!overlayStyleSheet.isEmpty() && !QFile::exists(overlayStyleSheet)) { // User did choose one of predefined stylesheets, we need to qualify it with namespace - overlayStyleSheet = QStringLiteral("overlay:%1").arg(overlayStyleSheet); + overlayStyleSheet = QStringLiteral("overlay:%1").arg(overlayStyleSheet); } return overlayStyleSheet; diff --git a/src/Gui/PreCompiled.h b/src/Gui/PreCompiled.h index 718190076b..4da9bd1cda 100644 --- a/src/Gui/PreCompiled.h +++ b/src/Gui/PreCompiled.h @@ -69,8 +69,10 @@ #include #include #include +#include #include #include +#include #include #include #include diff --git a/src/Gui/PreferencePackManager.cpp b/src/Gui/PreferencePackManager.cpp index 2bac18b0e2..d573a06eeb 100644 --- a/src/Gui/PreferencePackManager.cpp +++ b/src/Gui/PreferencePackManager.cpp @@ -41,9 +41,12 @@ #include "DockWindowManager.h" #include "ToolBarManager.h" +#include #include -#include // For generating a timestamped filename +#include // For generating a timestamped filename +#include +#include using namespace Gui; @@ -359,6 +362,9 @@ bool PreferencePackManager::apply(const std::string& preferencePackName) const Gui::ToolBarManager* pToolbarMgr = Gui::ToolBarManager::getInstance(); pToolbarMgr->restoreState(); + // We need to reload stylesheet to apply any changed style parameters + Gui::Application::Instance->reloadStyleSheet(); + // TODO: Are there other things that have to be manually triggered? } return wasApplied; diff --git a/src/Gui/PreferencePackTemplates/Shortcuts.cfg b/src/Gui/PreferencePackTemplates/Shortcuts.cfg index 63db87461b..1ef9b11b24 100644 --- a/src/Gui/PreferencePackTemplates/Shortcuts.cfg +++ b/src/Gui/PreferencePackTemplates/Shortcuts.cfg @@ -627,6 +627,7 @@ Del + Ctrl+F6 @@ -642,9 +643,7 @@ - - @@ -690,7 +689,6 @@ - Ctrl+Q End diff --git a/src/Gui/PreferencePages/DlgSettingsUI.cpp b/src/Gui/PreferencePages/DlgSettingsUI.cpp index 7774259798..e5d7974621 100644 --- a/src/Gui/PreferencePages/DlgSettingsUI.cpp +++ b/src/Gui/PreferencePages/DlgSettingsUI.cpp @@ -31,6 +31,10 @@ #include "DlgSettingsUI.h" #include "ui_DlgSettingsUI.h" +#include "Dialogs/DlgThemeEditor.h" + +#include + using namespace Gui::Dialog; @@ -45,6 +49,10 @@ DlgSettingsUI::DlgSettingsUI(QWidget* parent) , ui(new Ui_DlgSettingsUI) { ui->setupUi(this); + + connect(ui->themeEditorButton, &QPushButton::clicked, [this]() { + openThemeEditor(); + }); } /** @@ -114,13 +122,14 @@ void DlgSettingsUI::loadStyleSheet() populateStylesheets("OverlayActiveStyleSheet", "overlay", ui->OverlayStyleSheets, "Auto"); } -void DlgSettingsUI::populateStylesheets(const char *key, - const char *path, - PrefComboBox *combo, - const char *def, +void DlgSettingsUI::populateStylesheets(const char* key, + const char* path, + PrefComboBox* combo, + const char* def, QStringList filter) { - auto hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/MainWindow"); + auto hGrp = App::GetApplication().GetParameterGroupByPath( + "User parameter:BaseApp/Preferences/MainWindow"); // List all .qss/.css files QMap cssFiles; QDir dir; @@ -172,6 +181,12 @@ void DlgSettingsUI::populateStylesheets(const char *key, combo->onRestore(); } +void DlgSettingsUI::openThemeEditor() +{ + Gui::DlgThemeEditor editor; + editor.exec(); +} + /** * Sets the strings of the subwidgets using the current language. */ @@ -190,6 +205,10 @@ namespace { void applyStyleSheet(ParameterGrp *hGrp) { + if (auto parameterManager = Base::provideService()) { + parameterManager->reload(); + } + auto sheet = hGrp->GetASCII("StyleSheet"); bool tiledBG = hGrp->GetBool("TiledBackground", false); Gui::Application::Instance->setStyleSheet(QString::fromUtf8(sheet.c_str()), tiledBG); diff --git a/src/Gui/PreferencePages/DlgSettingsUI.h b/src/Gui/PreferencePages/DlgSettingsUI.h index 4dcfbea680..37030b5c4b 100644 --- a/src/Gui/PreferencePages/DlgSettingsUI.h +++ b/src/Gui/PreferencePages/DlgSettingsUI.h @@ -62,6 +62,7 @@ protected: const char *def, QStringList filter = QStringList()); + void openThemeEditor(); private: std::unique_ptr ui; }; diff --git a/src/Gui/PreferencePages/DlgSettingsUI.ui b/src/Gui/PreferencePages/DlgSettingsUI.ui index 5bf0459cd2..2f6f72d89e 100644 --- a/src/Gui/PreferencePages/DlgSettingsUI.ui +++ b/src/Gui/PreferencePages/DlgSettingsUI.ui @@ -31,7 +31,7 @@
- + @@ -53,7 +53,7 @@ This color might be used by your theme to let you customize it. - + 85 123 @@ -119,7 +119,7 @@ This color might be used by your theme to let you customize it. - + 85 123 @@ -145,7 +145,7 @@ This color might be used by your theme to let you customize it. - + 85 123 @@ -165,12 +165,12 @@ Style sheet how user interface will look like - - MainWindow - StyleSheet + + MainWindow + @@ -178,12 +178,12 @@ - - MainWindow - OverlayActiveStyleSheet + + MainWindow + @@ -191,6 +191,13 @@ + + + + Open Theme Editor + + +
@@ -230,12 +237,12 @@ 16 - - TreeView - IconSize + + TreeView + @@ -285,12 +292,12 @@ 0 - - TreeView - ItemSpacing + + TreeView + @@ -492,7 +499,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -510,26 +517,26 @@ QPushButton
Gui/Widgets.h
- - Gui::PrefColorButton - Gui::ColorButton -
Gui/PrefWidgets.h
-
- - Gui::PrefComboBox - QComboBox -
Gui/PrefWidgets.h
-
Gui::PrefSpinBox QSpinBox
Gui/PrefWidgets.h
+ + Gui::PrefColorButton + Gui::ColorButton +
Gui/PrefWidgets.h
+
Gui::PrefCheckBox QCheckBox
Gui/PrefWidgets.h
+ + Gui::PrefComboBox + QComboBox +
Gui/PrefWidgets.h
+
ThemeAccentColor1 diff --git a/src/Gui/StyleParameters/ParameterManager.cpp b/src/Gui/StyleParameters/ParameterManager.cpp new file mode 100644 index 0000000000..24c685b096 --- /dev/null +++ b/src/Gui/StyleParameters/ParameterManager.cpp @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kacper Donat * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD 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. * + * * + * FreeCAD 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 FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#include "ParameterManager.h" +#include "Parser.h" + +#ifndef _PreComp_ +#include +#include +#include +#include +#include +#endif + +namespace Gui::StyleParameters +{ + +Length Length::operator+(const Length& rhs) const +{ + ensureEqualUnits(rhs); + return {value + rhs.value, unit}; +} + +Length Length::operator-(const Length& rhs) const +{ + ensureEqualUnits(rhs); + return {value - rhs.value, unit}; +} + +Length Length::operator-() const +{ + return {-value, unit}; +} + +Length Length::operator/(const Length& rhs) const +{ + if (rhs.value == 0) { + THROWM(Base::RuntimeError, "Division by zero"); + } + + if (rhs.unit.empty() || unit.empty()) { + return {value / rhs.value, unit}; + } + + ensureEqualUnits(rhs); + return {value / rhs.value, unit}; +} + +Length Length::operator*(const Length& rhs) const +{ + if (rhs.unit.empty() || unit.empty()) { + return {value * rhs.value, unit}; + } + + ensureEqualUnits(rhs); + return {value * rhs.value, unit}; +} + +void Length::ensureEqualUnits(const Length& rhs) const +{ + if (unit != rhs.unit) { + THROWM(Base::RuntimeError, + fmt::format("Units mismatch left expression is '{}', right expression is '{}'", + unit, + rhs.unit)); + } +} + +std::string Value::toString() const +{ + if (std::holds_alternative(*this)) { + auto [value, unit] = std::get(*this); + return fmt::format("{}{}", value, unit); + } + + if (std::holds_alternative(*this)) { + auto color = std::get(*this); + return fmt::format("#{:0>6x}", 0xFFFFFF & color.rgb()); // NOLINT(*-magic-numbers) + } + + return std::get(*this); +} + +ParameterSource::ParameterSource(const Metadata& metadata) + : metadata(metadata) +{} + +InMemoryParameterSource::InMemoryParameterSource(const std::list& parameters, + const Metadata& metadata) + : ParameterSource(metadata) +{ + for (const auto& parameter : parameters) { + InMemoryParameterSource::define(parameter); + } +} + +std::list InMemoryParameterSource::all() const +{ + auto values = parameters | std::ranges::views::values; + + return std::list(values.begin(), values.end()); +} + +std::optional InMemoryParameterSource::get(const std::string& name) const +{ + if (parameters.contains(name)) { + return parameters.at(name); + } + + return std::nullopt; +} + +void InMemoryParameterSource::define(const Parameter& parameter) +{ + parameters[parameter.name] = parameter; +} + +void InMemoryParameterSource::remove(const std::string& name) +{ + parameters.erase(name); +} + +BuiltInParameterSource::BuiltInParameterSource(const Metadata& metadata) + : ParameterSource(metadata) +{ + this->metadata.options |= ReadOnly; +} + +std::list BuiltInParameterSource::all() const +{ + std::list result; + + for (const auto& name : params | std::views::keys) { + result.push_back(*get(name)); + } + + return result; +} + +std::optional BuiltInParameterSource::get(const std::string& name) const +{ + if (params.contains(name)) { + unsigned long color = params.at(name)->GetUnsigned(name.c_str(), 0); + + return Parameter { + .name = name, + .value = fmt::format("#{:0>6x}", 0x00FFFFFF & (color >> 8)), // NOLINT(*-magic-numbers) + }; + } + + return std::nullopt; +} + +UserParameterSource::UserParameterSource(ParameterGrp::handle hGrp, const Metadata& metadata) + : ParameterSource(metadata) + , hGrp(hGrp) +{} + +std::list UserParameterSource::all() const +{ + std::list result; + + for (const auto& [token, value] : hGrp->GetASCIIMap()) { + result.push_back({ + .name = token, + .value = value, + }); + } + + return result; +} + +std::optional UserParameterSource::get(const std::string& name) const +{ + if (auto value = hGrp->GetASCII(name.c_str(), ""); !value.empty()) { + return Parameter { + .name = name, + .value = value, + }; + } + + return {}; +} + +void UserParameterSource::define(const Parameter& parameter) +{ + hGrp->SetASCII(parameter.name.c_str(), parameter.value); +} + +void UserParameterSource::remove(const std::string& name) +{ + hGrp->RemoveASCII(name.c_str()); +} + +ParameterManager::ParameterManager() = default; + +void ParameterManager::reload() +{ + _resolved.clear(); +} + +std::string ParameterManager::replacePlaceholders(const std::string& expression, + ResolveContext context) const +{ + static const QRegularExpression regex(QStringLiteral("@(\\w+)")); + + auto substituteWithCallback = + [](const QRegularExpression& regex, + const QString& input, + const std::function& callback) { + QRegularExpressionMatchIterator it = regex.globalMatch(input); + + QString result; + qsizetype lastIndex = 0; + + while (it.hasNext()) { + QRegularExpressionMatch match = it.next(); + + qsizetype start = match.capturedStart(); + qsizetype end = match.capturedEnd(); + + result += input.mid(lastIndex, start - lastIndex); + result += callback(match); + + lastIndex = end; + } + + // Append any remaining text after the last match + result += input.mid(lastIndex); + + return result; + }; + + // clang-format off + return substituteWithCallback( + regex, + QString::fromStdString(expression), + [&](const QRegularExpressionMatch& match) { + auto tokenName = match.captured(1).toStdString(); + + auto tokenValue = resolve(tokenName, context); + context.visited.erase(tokenName); + + return QString::fromStdString(tokenValue.toString()); + } + ).toStdString(); + // clang-format on +} + +std::list ParameterManager::parameters() const +{ + std::set result; + + // we need to traverse it in reverse order so more important tokens will take precedence + for (const ParameterSource* source : _sources | std::views::reverse) { + for (const Parameter& parameter : source->all()) { + result.insert(parameter); + } + } + + return std::list(result.begin(), result.end()); +} + +std::optional ParameterManager::expression(const std::string& name) const +{ + if (auto param = parameter(name)) { + return param->value; + } + + return {}; +} + +Value ParameterManager::resolve(const std::string& name, ResolveContext context) const +{ + std::optional maybeParameter = this->parameter(name); + + if (!maybeParameter) { + Base::Console().warning("Requested non-existent design token '%s'.", name); + return std::string {}; + } + + if (context.visited.contains(name)) { + Base::Console().warning("The design token '%s' contains circular-reference.", name); + return expression(name).value_or(std::string {}); + } + + const Parameter& token = *maybeParameter; + + if (!_resolved.contains(token.name)) { + context.visited.insert(token.name); + try { + _resolved[token.name] = evaluate(token.value, context); + } + catch (Base::Exception&) { + // in case of being unable to parse it, we need to treat it as a generic value + _resolved[token.name] = replacePlaceholders(token.value, context); + } + context.visited.erase(token.name); + } + + return _resolved[token.name]; +} + +Value ParameterManager::evaluate(const std::string& expression, ResolveContext context) const +{ + Parser parser(expression); + return parser.parse()->evaluate({.manager = this, .context = std::move(context)}); +} + +std::optional ParameterManager::parameter(const std::string& name) const +{ + for (const ParameterSource* source : _sources) { + if (const auto& parameter = source->get(name)) { + return parameter; + } + } + + return {}; +} + +void ParameterManager::addSource(ParameterSource* source) +{ + _sources.push_front(source); +} + +std::list ParameterManager::sources() const +{ + return _sources; +} + +} // namespace Gui::StyleParameters \ No newline at end of file diff --git a/src/Gui/StyleParameters/ParameterManager.h b/src/Gui/StyleParameters/ParameterManager.h new file mode 100644 index 0000000000..8736755c99 --- /dev/null +++ b/src/Gui/StyleParameters/ParameterManager.h @@ -0,0 +1,457 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kacper Donat * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD 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. * + * * + * FreeCAD 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 FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#ifndef STYLEPARAMETERS_PARAMETERMANAGER_H +#define STYLEPARAMETERS_PARAMETERMANAGER_H + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace Gui::StyleParameters +{ + +// Forward declaration for Parser +class Parser; + +/** + * @brief Represents a length in a specified unit. + * + * This struct is a very simplified representation of lengths that can be used as parameters for + * styling purposes. The length basically consists of value and unit. Unit is optional, empty unit + * represents a dimensionless length that can be used as a scalar. This struct does not care about + * unit conversions as its uses do not require it. + */ +struct Length +{ + /// Numeric value of the length. + double value; + /// Unit of the length, empty if the value is dimensionless. + std::string unit; + + /** + * @name Operators + * + * This struct supports basic operations on Length. Each operation requires for operands to be + * the same unit. Multiplication and division additionally allow one operand to be dimensionless + * and hence act as a scalar. + * + * @code{c++} + * Length a { 10, "px" }; + * Length b { 5, "px" }; + * + * Length differentUnit { 3, "rem" } + * Length scalar { 2, "" }; + * + * // basic operations with the same unit are allowed + * auto sum = a + b; // 15 px + * auto difference = a - 5; // 10 px + * + * // basic operations with mixed units are NOT allowed + * auto sumOfIncompatibleUnits = a + differentUnit; // will throw + * auto productOfIncompatibleUnits = a * differentUnit; // will throw + * + * // exception is that for multiplication and division dimensionless units are allowed + * auto productWithScalar = a * scalar; // 20 px + * @endcode + * @{ + */ + Length operator+(const Length& rhs) const; + Length operator-(const Length& rhs) const; + Length operator-() const; + + Length operator/(const Length& rhs) const; + Length operator*(const Length& rhs) const; + /// @} + +private: + void ensureEqualUnits(const Length& rhs) const; +}; + +/** + * @brief This struct represents any valid value that can be used as the parameter value. + * + * The value can be one of three basic types: + * - Numbers / Lengths (so any length with optional unit) (Length) + * - Colors (QColor) + * - Any other generic expression. (std::string) + * + * As a rule, operations can be only performed over values of the same type. + */ +struct Value : std::variant +{ + using std::variant::variant; + + /** + * Converts the object into its string representation. + * + * @return A string representation of the object that can later be used in QSS. + */ + std::string toString() const; +}; + +/** + * @struct Parameter + * + * @brief Represents a named, dynamic expression-based parameter. + * + * The Parameter structure is used to define reusable named variables in styling or layout systems. + * Each parameter consists of a `name` and a `value` string, where the value is a CSS-like expression + * that supports numbers, units, arithmetic, colors, functions, and parameter references. + * + * ### Naming Convention + * Parameter names must be unique and follow **CamelCase**. + */ +struct Parameter +{ + /// Comparator that assumes that parameters are equal as long as name is the same + struct NameComparator + { + bool operator()(const Parameter& lhs, const Parameter& rhs) const + { + return lhs.name < rhs.name; + } + }; + + /// Name of the parameter, name should follow CamelCase + std::string name; + /// Expression associated with the parameter + std::string value; +}; + +enum class ParameterSourceOption +{ + // clang-format off + /// Parameters are read-only and the source does not allow editing + ReadOnly = 1 << 0, + /// Parameters are expected to be edited by the user, not only theme developers + UserEditable = 1 << 1, + // clang-format on +}; + +using ParameterSourceOptions = Base::Flags; + +/** + * @brief Abstract base class representing a source of style parameters. + * + * A `ParameterSource` is responsible for managing a collection of named parameters. Each source + * has metadata describing its type, characteristics, and behavior. + * + * ### Key Responsibilities + * - Define, update, and remove parameters within the source. + * - Provide access to all parameters or specific ones by name. + * - Act as a backend for parameter management, feeding the `ParameterManager` with available + * parameter data. + * + * ### Metadata + * Each parameter source includes metadata consisting of: + * - `name`: Name of the source, for identification purposes. + * - `options`: Flags specifying optional behavior (e.g., `ReadOnly`, `UserEditable`). + * + * ### Notes on Usage + * - Subclasses of `ParameterSource` (e.g., `BuiltInParameterSource`, `UserParameterSource`) + * implement different storage mechanisms and behaviors based on whether parameters are + * pre-defined, user-defined, or loaded in memory. + * - Parameters can be retrieved and manipulated globally through the `ParameterManager`, which + * aggregates multiple `ParameterSource` instances. + * + * #### Example + * @code{.cpp} + * // Create an in-memory parameter source + * InMemoryParameterSource source({ + * Parameter{ "BasePadding", "16px" }, + * Parameter{ "DefaultColor", "#ff00ff" }, + * }); + * + * source.define(Parameter{ "Margin", "4px" }); // Adds a new parameter + * + * auto padding = source.get("BasePadding"); // Retrieves parameter named "BasePadding" + * auto parametersList = source.all(); // Retrieve all parameters + * @endcode + * + * ### Subclass Requirements + * Derived classes must implement: + * - `all()` - Retrieve all parameters in the source. + * - `get()` - Retrieve a specific parameter. + * - `define()` - Add or update a parameter, can be left empty for readonly sources. + * - `remove()` - Remove a parameter by name, can be left empty for readonly sources. + */ +class GuiExport ParameterSource +{ +public: + using enum ParameterSourceOption; + + /** + * @brief Contains metadata information about a `ParameterSource`. + * + * The `Metadata` struct provides a way to describe the characteristics and identity + * of a `ParameterSource`. It includes a name for identification and a set of options + * that define the source's behavior and restrictions. + */ + struct Metadata + { + /// The name of the parameter source. Should be marked for translation using QT_TR_NOOP + std::string name; + /// Flags defining the behavior and properties of the parameter source. + ParameterSourceOptions options {}; + }; + + /// Metadata of the parameter source + Metadata metadata; + + FC_DEFAULT_MOVE(ParameterSource); + FC_DISABLE_COPY(ParameterSource); + + explicit ParameterSource(const Metadata& metadata); + virtual ~ParameterSource() = default; + + /** + * @brief Retrieves a list of all parameters available in the source. + * + * This method returns every parameter defined within this source, enabling iteration and bulk + * access to all stored values. + * + * @return A list containing all `Parameter` objects stored in the source. + */ + virtual std::list all() const = 0; + + /** + * @brief Retrieves a specific parameter by its name. + * + * @param[in] name The name of the parameter to retrieve. + * @return An optional containing the requested parameter if it exists, or empty if not. + */ + virtual std::optional get(const std::string& name) const = 0; + + /** + * @brief Defines or updates a parameter in the source. + * + * Adds a new parameter to the source if it doesn't already exist or updates the value of an + * existing parameter with the same name. + * + * @param[in] parameter The `Parameter` object to define or update in the source. + */ + virtual void define([[maybe_unused]] const Parameter& parameter) {} + + /** + * @brief Removes a parameter from the source by its name. + * + * Deletes the specific parameter from the source if it exists. If no parameter with the given + * name is found, the method does nothing. + * + * @param[in] name The name of the parameter to remove. + */ + virtual void remove([[maybe_unused]] const std::string& name) {} +}; + +/** + * @brief In-memory parameter source that stores parameters in a map. + * + * This source is useful for temporary parameter storage or when you need to + * define parameters programmatically without persisting them to disk. + */ +class GuiExport InMemoryParameterSource : public ParameterSource +{ + std::map parameters; + +public: + InMemoryParameterSource(const std::list& parameters, const Metadata& metadata); + + std::list all() const override; + std::optional get(const std::string& name) const override; + void define(const Parameter& parameter) override; + void remove(const std::string& name) override; +}; + +/** + * @brief Built-in parameter source that reads from FreeCAD's parameter system. + * + * This source provides access to predefined parameters that are stored in + * FreeCAD's global parameter system. These parameters are typically defined + * by the application and are read-only. + */ +class GuiExport BuiltInParameterSource : public ParameterSource +{ +public: + explicit BuiltInParameterSource(const Metadata& metadata = {}); + + std::list all() const override; + std::optional get(const std::string& name) const override; + +private: + ParameterGrp::handle hGrpThemes = + App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Themes"); + ParameterGrp::handle hGrpView = + App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/View"); + + std::map params = { + {"ThemeAccentColor1", hGrpThemes}, + {"ThemeAccentColor2", hGrpThemes}, + {"ThemeAccentColor3", hGrpThemes}, + {"BackgroundColor", hGrpView}, + }; +}; + +/** + * @brief User-defined parameter source that reads from user preferences. + * + * This source provides access to user-defined parameters that are stored + * in the user's preference file. These parameters can be modified by the + * user and persist across application sessions. + */ +class GuiExport UserParameterSource : public ParameterSource +{ + ParameterGrp::handle hGrp; + +public: + UserParameterSource(ParameterGrp::handle hGrp, const Metadata& metadata); + + std::list all() const override; + std::optional get(const std::string& name) const override; + void define(const Parameter& parameter) override; + void remove(const std::string& name) override; +}; + +/** + * @brief Central manager for style parameters that aggregates multiple sources. + * + * The ParameterManager is responsible for: + * - Managing multiple parameter sources + * - Resolving parameter references and expressions + * - Caching resolved values for performance + * - Handling circular references + */ +class GuiExport ParameterManager +{ + std::list _sources; + mutable std::map _resolved; + +public: + struct ResolveContext + { + /// Names of parameters currently being resolved. + std::set visited; + }; + + ParameterManager(); + + /** + * @brief Clears the internal cache of resolved values. + * + * Call this method when parameters have been modified to ensure + * that the changes are reflected in subsequent resolutions. + */ + void reload(); + + /** + * @brief Replaces parameter placeholders in a string with their resolved values. + * + * This method performs simple string substitution of @parameter references + * with their actual values. It does not evaluate expressions, only performs + * direct substitution. + * + * @param expression The string containing parameter placeholders + * @param context Resolution context for handling circular references + * @return The string with all placeholders replaced + */ + std::string replacePlaceholders(const std::string& expression, ResolveContext context = {}) const; + + /** + * @brief Returns all available parameters from all sources. + * + * Parameters are returned in order of source priority, with later sources + * taking precedence over earlier ones. + * + * @return List of all available parameters + */ + std::list parameters() const; + + /** + * @brief Gets the raw expression string for a parameter. + * + * @param name The name of the parameter + * @return The expression string if the parameter exists, empty otherwise + */ + std::optional expression(const std::string& name) const; + + /** + * @brief Resolves a parameter to its final value. + * + * This method evaluates the parameter's expression and returns the computed + * value. The result is cached for subsequent calls. + * + * @param name The name of the parameter to resolve + * @param context Resolution context for handling circular references + * @return The resolved value + */ + Value resolve(const std::string& name, ResolveContext context = {}) const; + + /** + * @brief Evaluates an expression string and returns the result. + * + * @param expression The expression to evaluate + * @param context Resolution context for handling circular references + * @return The evaluated value + */ + Value evaluate(const std::string& expression, ResolveContext context = {}) const; + + /** + * @brief Gets a parameter by name from any source. + * + * @param name The name of the parameter + * @return The parameter if found, empty otherwise + */ + std::optional parameter(const std::string& name) const; + + /** + * @brief Adds a parameter source to the manager. + * + * Sources are evaluated in the order they are added, with later sources + * taking precedence over earlier ones. + * + * @param source The parameter source to add + */ + void addSource(ParameterSource* source); + + /** + * @brief Returns all registered parameter sources. + * + * @return List of parameter sources in order of registration + */ + std::list sources() const; +}; + +} // namespace Gui::StyleParameters + +ENABLE_BITMASK_OPERATORS(Gui::StyleParameters::ParameterSourceOption); + +#endif // STYLEPARAMETERS_PARAMETERMANAGER_H \ No newline at end of file diff --git a/src/Gui/StyleParameters/Parser.cpp b/src/Gui/StyleParameters/Parser.cpp new file mode 100644 index 0000000000..dbcb52f27a --- /dev/null +++ b/src/Gui/StyleParameters/Parser.cpp @@ -0,0 +1,403 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kacper Donat * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD 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. * + * * + * FreeCAD 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 FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#include "Parser.h" +#include "ParameterManager.h" + +#ifndef _PreComp_ +#include +#include +#include +#include +#include +#endif + +namespace Gui::StyleParameters +{ + +Value ParameterReference::evaluate(const EvaluationContext& context) const +{ + return context.manager->resolve(name, context.context); +} + +Value Number::evaluate([[maybe_unused]] const EvaluationContext& context) const +{ + return value; +} + +Value Color::evaluate([[maybe_unused]] const EvaluationContext& context) const +{ + return color; +} + +Value FunctionCall::evaluate(const EvaluationContext& context) const +{ + if (arguments.size() != 2) { + THROWM(Base::ExpressionError, + fmt::format("Function '{}' expects 2 arguments, got {}", + functionName, + arguments.size())); + } + + auto colorArg = arguments[0]->evaluate(context); + auto amountArg = arguments[1]->evaluate(context); + + if (!std::holds_alternative(colorArg)) { + THROWM(Base::ExpressionError, + fmt::format("'{}' is not supported for colors", functionName)); + } + + auto color = std::get(colorArg); + + // In Qt if you want to make color 20% darker or lighter, you need to pass 120 as the value + // we, however, want users to pass only the relative difference, hence we need to add the + // 100 required by Qt. + // + // NOLINTNEXTLINE(*-magic-numbers) + auto amount = 100 + static_cast(std::get(amountArg).value); + + if (functionName == "lighten") { + return color.lighter(amount); + } + + if (functionName == "darken") { + return color.darker(amount); + } + + THROWM(Base::ExpressionError, fmt::format("Unknown function '{}'", functionName)); +} + +Value BinaryOp::evaluate(const EvaluationContext& context) const +{ + Value lval = left->evaluate(context); + Value rval = right->evaluate(context); + + if (!std::holds_alternative(lval) || !std::holds_alternative(rval)) { + THROWM(Base::ExpressionError, "Math operations are supported only on lengths"); + } + + auto lhs = std::get(lval); + auto rhs = std::get(rval); + + switch (op) { + case Operator::Add: + return lhs + rhs; + case Operator::Subtract: + return lhs - rhs; + case Operator::Multiply: + return lhs * rhs; + case Operator::Divide: + return lhs / rhs; + default: + THROWM(Base::ExpressionError, "Unknown operator"); + } +} + +Value UnaryOp::evaluate(const EvaluationContext& context) const +{ + Value val = operand->evaluate(context); + if (std::holds_alternative(val)) { + THROWM(Base::ExpressionError, "Unary operations on colors are not supported"); + } + + auto v = std::get(val); + switch (op) { + case Operator::Add: + return v; + case Operator::Subtract: + return -v; + default: + THROWM(Base::ExpressionError, "Unknown unary operator"); + } +} + +std::unique_ptr Parser::parse() +{ + auto expr = parseExpression(); + skipWhitespace(); + if (pos != input.size()) { + THROWM(Base::ParserError, + fmt::format("Unexpected characters at end of input: {}", input.substr(pos))); + } + return expr; +} + +bool Parser::peekString(const char* function) const +{ + return input.compare(pos, strlen(function), function) == 0; +} + +std::unique_ptr Parser::parseExpression() +{ + auto expr = parseTerm(); + while (true) { + skipWhitespace(); + if (match('+')) { + expr = std::make_unique(std::move(expr), Operator::Add, parseTerm()); + } + else if (match('-')) { + expr = std::make_unique(std::move(expr), Operator::Subtract, parseTerm()); + } + else { + break; + } + } + return expr; +} + +std::unique_ptr Parser::parseTerm() +{ + auto expr = parseFactor(); + while (true) { + skipWhitespace(); + if (match('*')) { + expr = std::make_unique(std::move(expr), Operator::Multiply, parseFactor()); + } + else if (match('/')) { + expr = std::make_unique(std::move(expr), Operator::Divide, parseFactor()); + } + else { + break; + } + } + return expr; +} + +std::unique_ptr Parser::parseFactor() +{ + skipWhitespace(); + if (match('+') || match('-')) { + Operator op = (input[pos - 1] == '+') ? Operator::Add : Operator::Subtract; + return std::make_unique(op, parseFactor()); + } + if (match('(')) { + auto expr = parseExpression(); + if (!match(')')) { + THROWM(Base::ParserError, fmt::format("Expected ')', got '{}'", input[pos])); + } + return expr; + } + if (peekColor()) { + return parseColor(); + } + if (peekParameter()) { + return parseParameter(); + } + if (peekFunction()) { + return parseFunctionCall(); + } + return parseNumber(); +} + +bool Parser::peekColor() +{ + skipWhitespace(); + // clang-format off + return input[pos] == '#' + || peekString(rgbFunction) + || peekString(rgbaFunction); + // clang-format on +} + +std::unique_ptr Parser::parseColor() +{ + const auto parseHexadecimalColor = [&]() { + constexpr int hexadecimalBase = 16; + + // Format is #RRGGBB + pos++; + int r = std::stoi(input.substr(pos, 2), nullptr, hexadecimalBase); + pos += 2; + int g = std::stoi(input.substr(pos, 2), nullptr, hexadecimalBase); + pos += 2; + int b = std::stoi(input.substr(pos, 2), nullptr, hexadecimalBase); + pos += 2; + + return std::make_unique(QColor(r, g, b)); + }; + + const auto parseFunctionStyleColor = [&]() { + bool hasAlpha = peekString(rgbaFunction); + + pos += hasAlpha ? strlen(rgbaFunction) : strlen(rgbFunction); + + int r = parseInt(); + if (!match(',')) { + THROWM(Base::ParserError, fmt::format("Expected ',' after red, got '{}'", input[pos])); + } + int g = parseInt(); + if (!match(',')) { + THROWM(Base::ParserError, fmt::format("Expected ',' after green, got '{}'", input[pos])); + } + int b = parseInt(); + int a = 255; // NOLINT(*-magic-numbers) + if (hasAlpha) { + if (!match(',')) { + THROWM(Base::ParserError, fmt::format("Expected ',' after blue, got '{}'", input[pos])); + } + a = parseInt(); + } + if (!match(')')) { + THROWM(Base::ParserError, fmt::format("Expected ')' after color arguments, got '{}'", input[pos])); + } + return std::make_unique(QColor(r, g, b, a)); + }; + + skipWhitespace(); + + try { + if (input[pos] == '#') { + return parseHexadecimalColor(); + } + + if (peekString(rgbFunction) || peekString(rgbaFunction)) { + return parseFunctionStyleColor(); + } + } catch (std::invalid_argument&) { + THROWM(Base::ParserError, "Invalid color format, expected #RRGGBB or rgb(r,g,b) or rgba(r,g,b,a)"); + } + + THROWM(Base::ParserError, "Unknown color format"); +} + +bool Parser::peekParameter() +{ + skipWhitespace(); + return pos < input.size() && input[pos] == '@'; +} + +std::unique_ptr Parser::parseParameter() +{ + skipWhitespace(); + if (!match('@')) { + THROWM(Base::ParserError, fmt::format("Expected '@' for parameter, got '{}'", input[pos])); + } + size_t start = pos; + while (pos < input.size() && (isalnum(input[pos]) || input[pos] == '_')) { + ++pos; + } + if (start == pos) { + THROWM(Base::ParserError, + fmt::format("Expected parameter name after '@', got '{}'", input[pos])); + } + return std::make_unique(input.substr(start, pos - start)); +} + +bool Parser::peekFunction() +{ + skipWhitespace(); + return pos < input.size() && isalpha(input[pos]); +} + +std::unique_ptr Parser::parseFunctionCall() +{ + skipWhitespace(); + size_t start = pos; + while (pos < input.size() && isalnum(input[pos])) { + ++pos; + } + std::string functionName = input.substr(start, pos - start); + + if (!match('(')) { + THROWM(Base::ParserError, + fmt::format("Expected '(' after function name, got '{}'", input[pos])); + } + + std::vector> arguments; + if (!match(')')) { + do { // NOLINT(*-avoid-do-while) + arguments.push_back(parseExpression()); + } while (match(',')); + + if (!match(')')) { + THROWM(Base::ParserError, + fmt::format("Expected ')' after function arguments, got '{}'", input[pos])); + } + } + + return std::make_unique(functionName, std::move(arguments)); +} + +int Parser::parseInt() +{ + skipWhitespace(); + size_t start = pos; + while (pos < input.size() && (isdigit(input[pos]) || input[pos] == '.')) { + ++pos; + } + return std::stoi(input.substr(start, pos - start)); +} + +std::unique_ptr Parser::parseNumber() +{ + skipWhitespace(); + size_t start = pos; + while (pos < input.size() && (isdigit(input[pos]) || input[pos] == '.')) { + ++pos; + } + + std::string number = input.substr(start, pos - start); + + try { + double value = std::stod(number); + std::string unit = parseUnit(); + return std::make_unique(value, unit); + } + catch (std::invalid_argument& e) { + THROWM(Base::ParserError, fmt::format("Invalid number: {}", number)); + } +} + +std::string Parser::parseUnit() +{ + skipWhitespace(); + size_t start = pos; + while (pos < input.size() && (isalpha(input[pos]) || input[pos] == '%')) { + ++pos; + } + if (start == pos) { + return ""; + } + return input.substr(start, pos - start); +} + +bool Parser::match(char expected) +{ + skipWhitespace(); + if (pos < input.size() && input[pos] == expected) { + ++pos; + return true; + } + return false; +} + +void Parser::skipWhitespace() +{ + while (pos < input.size() && isspace(input[pos])) { + ++pos; + } +} + +} // namespace Gui::StyleParameters \ No newline at end of file diff --git a/src/Gui/StyleParameters/Parser.h b/src/Gui/StyleParameters/Parser.h new file mode 100644 index 0000000000..3e7867cae9 --- /dev/null +++ b/src/Gui/StyleParameters/Parser.h @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kacper Donat * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD 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. * + * * + * FreeCAD 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 FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#ifndef STYLEPARAMETERS_PARSER_H +#define STYLEPARAMETERS_PARSER_H + +#include +#include +#include + +#include "ParameterManager.h" + +namespace Gui::StyleParameters +{ + +enum class Operator : std::uint8_t +{ + Add, + Subtract, + Multiply, + Divide +}; + +struct EvaluationContext +{ + const ParameterManager* manager {}; + ParameterManager::ResolveContext context; +}; + +// Abstract Syntax Tree (AST) Base +struct GuiExport Expr +{ + Expr() = default; + + FC_DEFAULT_MOVE(Expr); + FC_DISABLE_COPY(Expr); + + virtual Value evaluate(const EvaluationContext& context) const = 0; + virtual ~Expr() = default; +}; + +struct GuiExport ParameterReference: public Expr +{ + std::string name; + + explicit ParameterReference(std::string name) + : name(std::move(name)) + {} + + Value evaluate(const EvaluationContext& context) const override; +}; + +struct GuiExport Number: public Expr +{ + Length value; + + Number(double value, std::string unit) + : value({value, std::move(unit)}) + {} + + Value evaluate([[maybe_unused]] const EvaluationContext& context) const override; +}; + +struct GuiExport Color: public Expr +{ + QColor color; + + explicit Color(QColor color) + : color(std::move(color)) + {} + + Value evaluate([[maybe_unused]] const EvaluationContext& context) const override; +}; + +struct GuiExport FunctionCall: public Expr +{ + std::string functionName; + std::vector> arguments; + + FunctionCall(std::string functionName, std::vector> arguments) + : functionName(std::move(functionName)) + , arguments(std::move(arguments)) + {} + + Value evaluate(const EvaluationContext& context) const override; +}; + +struct GuiExport BinaryOp: public Expr +{ + std::unique_ptr left, right; + Operator op; + + BinaryOp(std::unique_ptr left, Operator op, std::unique_ptr right) + : left(std::move(left)) + , right(std::move(right)) + , op(op) + {} + + Value evaluate(const EvaluationContext& context) const override; +}; + +struct GuiExport UnaryOp: public Expr +{ + Operator op; + std::unique_ptr operand; + + UnaryOp(Operator op, std::unique_ptr operand) + : op(op) + , operand(std::move(operand)) + {} + + Value evaluate(const EvaluationContext& context) const override; +}; + +class GuiExport Parser +{ + static constexpr auto rgbFunction = "rgb("; + static constexpr auto rgbaFunction = "rgba("; + + std::string input; + size_t pos = 0; + +public: + explicit Parser(std::string input) + : input(std::move(input)) + {} + + std::unique_ptr parse(); + +private: + bool peekString(const char* function) const; + std::unique_ptr parseExpression(); + std::unique_ptr parseTerm(); + std::unique_ptr parseFactor(); + bool peekColor(); + std::unique_ptr parseColor(); + bool peekParameter(); + std::unique_ptr parseParameter(); + bool peekFunction(); + std::unique_ptr parseFunctionCall(); + int parseInt(); + std::unique_ptr parseNumber(); + std::string parseUnit(); + bool match(char expected); + void skipWhitespace(); +}; + +} // namespace Gui::StyleParameters + +#endif // STYLEPARAMETERS_PARSER_H \ No newline at end of file diff --git a/src/Gui/Tools.h b/src/Gui/Tools.h index 29f5741a68..364da69ff1 100644 --- a/src/Gui/Tools.h +++ b/src/Gui/Tools.h @@ -1,5 +1,6 @@ /*************************************************************************** * Copyright (c) 2020 Werner Mayer * + * Copyright (c) 2025 Kacper Donat * * * * This file is part of the FreeCAD CAx development system. * * * @@ -23,39 +24,100 @@ #ifndef GUI_TOOLS_H #define GUI_TOOLS_H +#include + #include #include #include -#include +#include -namespace Gui { - -/*! - * \brief The QtTools class - * Helper class to reduce adding a lot of extra QT_VERSION checks to client code. +/** + * @brief The QtTools namespace + * + * Helper namespace to provide utilities to ease work with Qt. */ -class GuiExport QtTools { -public: - static int horizontalAdvance(const QFontMetrics& fm, QChar ch) { +namespace Gui::QtTools +{ +inline int horizontalAdvance(const QFontMetrics& fm, QChar ch) +{ + return fm.horizontalAdvance(ch); +} - return fm.horizontalAdvance(ch); - } - static int horizontalAdvance(const QFontMetrics& fm, const QString& text, int len = -1) { - return fm.horizontalAdvance(text, len); - } - static bool matches(QKeyEvent* ke, const QKeySequence& ks) { - uint searchkey = (ke->modifiers() | ke->key()) & ~(Qt::KeypadModifier | Qt::GroupSwitchModifier); - return ks == QKeySequence(searchkey); - } - static QKeySequence::StandardKey deleteKeySequence() { +inline int horizontalAdvance(const QFontMetrics& fm, const QString& text, int len = -1) +{ + return fm.horizontalAdvance(text, len); +} + +inline bool matches(QKeyEvent* ke, const QKeySequence& ks) +{ + uint searchkey = + (ke->modifiers() | ke->key()) & ~(Qt::KeypadModifier | Qt::GroupSwitchModifier); + return ks == QKeySequence(searchkey); +} + +inline QKeySequence::StandardKey deleteKeySequence() { #ifdef FC_OS_MACOSX - return QKeySequence::Backspace; + return QKeySequence::Backspace; #else - return QKeySequence::Delete; + return QKeySequence::Delete; #endif +} + +// clang-format off +/** + * TreeWalkCallable is a function that takes const QModelIndex& and: + * - returns void, if there is no stopping logic; + * - returns boolean, if there is logic that should stop tree traversal. + */ +template +concept TreeWalkCallable = + std::is_invocable_r_v || + std::is_invocable_r_v; +// clang-format on + +/** + * @brief Recursively traverses a QAbstractItemModel tree structure. + * + * The function traverses a tree model starting from a given index, or the root + * if no index is provided. For each node, it invokes the provided callable `func`. + * + * The callable can: + * - Return `void`, in which case the traversal continues through all nodes. + * - Return `bool`, in which case returning `true` stops further traversal. + * + * @param[in] model The tree model to traverse. + * @param[in] func A callable object applied to each QModelIndex. It can either + * return `void` or `bool` (for stopping logic). + * @param[in] index The starting index for traversal. If omitted, defaults to the root. + */ +void walkTreeModel(const QAbstractItemModel* model, + TreeWalkCallable auto&& func, + const QModelIndex& index = {}) +{ + using ReturnType = std::invoke_result_t; + + if (index.isValid()) { + if constexpr (std::is_same_v) { + func(index); + } + else if constexpr (std::is_same_v) { + if (func(index)) { + return; + } + } } -}; -} // namespace Gui + for (int i = 0; i < model->rowCount(index); ++i) { + walkTreeModel(model, func, model->index(i, 0, index)); + } +} -#endif // GUI_TOOLS_H +template +T valueOr(const QVariant& variant, const T& defaultValue) +{ + return variant.canConvert() ? variant.value() : defaultValue; +} + +} // namespace Gui::QtTools + +#endif // GUI_TOOLS_H diff --git a/src/Gui/ViewProviderLink.cpp b/src/Gui/ViewProviderLink.cpp index 28f8f71f6c..8073c7b51e 100644 --- a/src/Gui/ViewProviderLink.cpp +++ b/src/Gui/ViewProviderLink.cpp @@ -673,8 +673,11 @@ public: QIcon getIcon(QPixmap px) { static int iconSize = -1; - if(iconSize < 0) - iconSize = QApplication::style()->standardPixmap(QStyle::SP_DirClosedIcon).width(); + if (iconSize < 0) { + auto sampleIcon = QApplication::style()->standardPixmap(QStyle::SP_DirClosedIcon); + double pixelRatio = sampleIcon.devicePixelRatio(); + iconSize = static_cast(sampleIcon.width() / pixelRatio); + } if(!isLinked()) return QIcon(); diff --git a/src/Gui/Workbench.cpp b/src/Gui/Workbench.cpp index 5acad2bc8f..5adecc1ce0 100644 --- a/src/Gui/Workbench.cpp +++ b/src/Gui/Workbench.cpp @@ -767,11 +767,11 @@ MenuItem* StdWorkbench::setupMenuBar() const // Help auto help = new MenuItem( menuBar ); help->setCommand("&Help"); - *help << "Std_OnlineHelp" << "Std_WhatsThis" << "Separator" + *help << "Std_WhatsThis" << "Separator" // Start page and additional separator are dynamically inserted here - << "Std_FreeCADUserHub" << "Std_FreeCADForum" << "Std_FreeCADFAQ" << "Std_ReportBug" << "Separator" + << "Std_FreeCADUserHub" << "Std_FreeCADForum" << "Std_ReportBug" << "Separator" << "Std_RestartInSafeMode" << "Separator" - << "Std_FreeCADPowerUserHub" << "Std_PythonHelp" << "Separator" + << "Std_DevHandbook" << "Std_PythonHelp" << "Separator" << "Std_FreeCADWebsite" << "Std_FreeCADDonation" << "Std_About"; return menuBar; diff --git a/src/Mod/BIM/Arch.py b/src/Mod/BIM/Arch.py index 979ff4c888..614ca38cb5 100644 --- a/src/Mod/BIM/Arch.py +++ b/src/Mod/BIM/Arch.py @@ -794,7 +794,12 @@ def makePipeConnector(pipes, radius=0, name=None): # Initialize all relevant properties pipeConnector.Pipes = pipes - pipeConnector.Radius = radius if radius else pipes[0].Diameter + if radius: + pipeConnector.Radius = radius + elif pipes[0].ProfileType == "Circle": + pipeConnector.Radius = pipes[0].Diameter + else: + pipeConnector.Radius = max(pipes[0].Height, pipes[0].Width) return pipeConnector @@ -1686,7 +1691,7 @@ def makeWall( return wall -def joinWalls(walls, delete=False): +def joinWalls(walls, delete=False, deletebase=False): """Join the given list of walls into one sketch-based wall. Take the first wall in the list, and adds on the other walls in the list. @@ -1702,6 +1707,9 @@ def joinWalls(walls, delete=False): be based off a base object. delete : bool, optional If True, deletes the other walls in the list. Defaults to False. + deletebase : bool, optional + If True, and delete is True, the base of the other walls is also deleted + Defaults to False. Returns ------- @@ -1740,14 +1748,39 @@ def joinWalls(walls, delete=False): else: sk = base.Base for w in walls: - if w.Base: - if not w.Base.Shape.Faces: - for e in w.Base.Shape.Edges: - l = e.Curve - if isinstance(l, Part.Line): - l = Part.LineSegment(e.Vertexes[0].Point, e.Vertexes[-1].Point) - sk.addGeometry(l) - deleteList.append(w.Name) + if w.Base and not w.Base.Shape.Faces: + for hostedObj in w.Proxy.getHosts(w): + if hasattr(hostedObj, "Host"): + hostedObj.Host = base + else: + tmp = hostedObj.Hosts + if delete: + tmp.remove(w) + if not base in tmp: + tmp.append(base) + hostedObj.Hosts = tmp + tmp = [] + for add in w.Additions: + if not add in base.Additions: + tmp.append(add) + if delete: + w.Additions = None + base.Additions += tmp + tmp = [] + for sub in w.Subtractions: + if not sub in base.Subtractions: + tmp.append(sub) + if delete: + w.Subtractions = None + base.Subtractions += tmp + for e in w.Base.Shape.Edges: + l = e.Curve + if isinstance(l, Part.Line): + l = Part.LineSegment(e.Vertexes[0].Point, e.Vertexes[-1].Point) + sk.addGeometry(l) + deleteList.append(w.Name) + if deletebase: + deleteList.append(w.Base.Name) if delete: for n in deleteList: FreeCAD.ActiveDocument.removeObject(n) @@ -1933,4 +1966,4 @@ def _initializeArchObject( FreeCAD.Console.PrintError(f"Failed to import module '{moduleName}': {e}\n") return None - return obj \ No newline at end of file + return obj diff --git a/src/Mod/BIM/ArchCommands.py b/src/Mod/BIM/ArchCommands.py index 9595af1a1a..57420587bf 100644 --- a/src/Mod/BIM/ArchCommands.py +++ b/src/Mod/BIM/ArchCommands.py @@ -93,6 +93,14 @@ def getDefaultColor(objectType): r, g, b, _ = Draft.get_rgba_tuple(c) return (r, g, b, alpha) +def _usedForAttachment(host,obj): + if not getattr(obj,"AttachmentSupport",[]): + return False + for sub in obj.AttachmentSupport: + if sub[0] == host: + return True + return False + def addComponents(objectsList,host): '''addComponents(objectsList,hostObject): adds the given object or the objects from the given list as components to the given host Object. Use this for @@ -105,24 +113,32 @@ def addComponents(objectsList,host): host.addObject(o) elif hostType in ["Wall","CurtainWall","Structure","Precast","Window","Roof","Stairs","StructuralSystem","Panel","Component","Pipe"]: import DraftGeomUtils + outList = host.OutListRecursive a = host.Additions - if hasattr(host,"Axes"): - x = host.Axes + x = getattr(host,"Axes",[]) for o in objectsList: - if hasattr(o,'Shape'): + if hasattr(o,"Shape"): if Draft.getType(o) == "Window": if hasattr(o,"Hosts"): if not host in o.Hosts: g = o.Hosts g.append(host) o.Hosts = g - elif DraftGeomUtils.isValidPath(o.Shape) and (hostType in ["Structure","Precast"]): - if o.AttachmentSupport == host: - o.AttachmentSupport = None - host.Tool = o + elif o in outList: + FreeCAD.Console.PrintWarning( + translate( + "Arch", + "Cannot add {0} as it is already referenced by {1}." + ).format(o.Label, host.Label) + "\n" + ) elif Draft.getType(o) == "Axis": if not o in x: x.append(o) + elif DraftGeomUtils.isValidPath(o.Shape) and (hostType in ["Structure","Precast"]): + if _usedForAttachment(host,o): + o.AttachmentSupport = None + o.MapMode = "Deactivated" + host.Tool = o elif not o in a: if hasattr(o,"Shape"): a.append(o) @@ -148,15 +164,15 @@ def removeComponents(objectsList,host=None): objectsList = [objectsList] if host: if Draft.getType(host) in ["Wall","CurtainWall","Structure","Precast","Window","Roof","Stairs","StructuralSystem","Panel","Component","Pipe"]: - if hasattr(host,"Tool"): - if objectsList[0] == host.Tool: - host.Tool = None + if getattr(host,"Tool",None) in objectsList: + host.Tool = None if hasattr(host,"Axes"): a = host.Axes for o in objectsList[:]: if o in a: a.remove(o) objectsList.remove(o) + host.Axes = a s = host.Subtractions for o in objectsList: if Draft.getType(o) == "Window": @@ -168,34 +184,24 @@ def removeComponents(objectsList,host=None): elif not o in s: s.append(o) if FreeCAD.GuiUp: - if not Draft.getType(o) in ["Window","Roof"]: + if Draft.getType(o) != "Roof": setAsSubcomponent(o) - # Making reference to BimWindow.Arch_Window: - # Check if o and o.Base has Attachment Support, and - # if the support is the host object itself - thus a cyclic - # dependency and probably creating TNP. - # If above is positive, remove its AttachmentSupport: + # Avoid cyclic dependency via Attachment Support: if hasattr(o,"Base") and o.Base: objList = [o, o.Base] else: objList = [o] for i in objList: - objHost = None - if hasattr(i,"AttachmentSupport"): - if i.AttachmentSupport: - if isinstance(i.AttachmentSupport,tuple): - objHost = i.AttachmentSupport[0] - elif isinstance(i.AttachmentSupport,list): - objHost = i.AttachmentSupport[0][0] - else: - objHost = i.AttachmentSupport - if objHost == host: - msg = FreeCAD.Console.PrintMessage - msg(i.Label + " is mapped to " + host.Label + - ", removing the former's Attachment " + - "Support to avoid cyclic dependency and " + - "TNP." + "\n") - i.AttachmentSupport = None # remove + if _usedForAttachment(host,i): + FreeCAD.Console.PrintMessage( + translate( + "Arch", + "{0} is mapped to {1}, removing the former's " + + "Attachment Support to avoid cyclic dependency." + ).format(o.Label, host.Label) + "\n" + ) + i.AttachmentSupport = None + i.MapMode = "Deactivated" host.Subtractions = s elif Draft.getType(host) in ["SectionPlane"]: a = host.Objects diff --git a/src/Mod/BIM/ArchComponent.py b/src/Mod/BIM/ArchComponent.py index 7ca305eb66..836ff2c01b 100644 --- a/src/Mod/BIM/ArchComponent.py +++ b/src/Mod/BIM/ArchComponent.py @@ -782,7 +782,7 @@ class Component(ArchIFC.IfcProduct): subvolume = o.getLinkedObject().Proxy.getSubVolume(o,host=obj) # pass host obj (mostly Wall) elif (Draft.getType(o) == "Roof") or (Draft.isClone(o,"Roof")): # roofs define their own special subtraction volume - subvolume = o.Proxy.getSubVolume(o) + subvolume = o.Proxy.getSubVolume(o).copy() elif hasattr(o,"Subvolume") and hasattr(o.Subvolume,"Shape"): # Any other object with a Subvolume property ## TODO - Part.Shape() instead? @@ -1413,18 +1413,13 @@ class ViewProviderComponent: The name of the property that has changed. """ - #print(vobj.Object.Name, " : changing ",prop) - #if prop == "Visibility": - #for obj in vobj.Object.Additions+vobj.Object.Subtractions: - # if (Draft.getType(obj) == "Window") or (Draft.isClone(obj,"Window",True)): - # obj.ViewObject.Visibility = vobj.Visibility - # this would now hide all previous windows... Not the desired behaviour anymore. + obj = vobj.Object if prop == "DiffuseColor": - if hasattr(vobj.Object,"CloneOf"): - if vobj.Object.CloneOf and hasattr(vobj.Object.CloneOf,"DiffuseColor"): - if len(vobj.Object.CloneOf.ViewObject.DiffuseColor) > 1: - if vobj.DiffuseColor != vobj.Object.CloneOf.ViewObject.DiffuseColor: - vobj.DiffuseColor = vobj.Object.CloneOf.ViewObject.DiffuseColor + if hasattr(obj,"CloneOf"): + if obj.CloneOf and hasattr(obj.CloneOf,"DiffuseColor"): + if len(obj.CloneOf.ViewObject.DiffuseColor) > 1: + if vobj.DiffuseColor != obj.CloneOf.ViewObject.DiffuseColor: + vobj.DiffuseColor = obj.CloneOf.ViewObject.DiffuseColor vobj.update() elif prop == "ShapeColor": # restore DiffuseColor after overridden by ShapeColor @@ -1433,10 +1428,16 @@ class ViewProviderComponent: d = vobj.DiffuseColor vobj.DiffuseColor = d elif prop == "Visibility": - for host in vobj.Object.Proxy.getHosts(vobj.Object): - if hasattr(host, 'ViewObject'): - host.ViewObject.Visibility = vobj.Visibility - + # do nothing if object is an addition + if not [parent for parent in obj.InList if obj in getattr(parent, "Additions", [])]: + hostedObjs = obj.Proxy.getHosts(obj) + # add objects hosted by additions + for addition in getattr(obj, "Additions", []): + if hasattr(addition, "Proxy") and hasattr(addition.Proxy, "getHosts"): + hostedObjs.extend(addition.Proxy.getHosts(addition)) + for hostedObj in hostedObjs: + if hasattr(hostedObj, "ViewObject"): + hostedObj.ViewObject.Visibility = vobj.Visibility return def attach(self,vobj): diff --git a/src/Mod/BIM/ArchProject.py b/src/Mod/BIM/ArchProject.py index 7374824f0f..6d16ad1c00 100644 --- a/src/Mod/BIM/ArchProject.py +++ b/src/Mod/BIM/ArchProject.py @@ -137,9 +137,12 @@ class _ViewProviderProject(ArchIFCView.IfcContextView): https://forum.freecad.org/viewtopic.php?f=10&t=74731 """ + from pivy import coin + from draftutils import gui_utils + if not hasattr(self, "displaymodes_cleaned"): - if vobj.RootNode.getNumChildren() > 2: - main_switch = vobj.RootNode.getChild(2) # The display mode switch. + if vobj.RootNode.getNumChildren(): + main_switch = gui_utils.find_coin_node(vobj.RootNode, coin.SoSwitch) # The display mode switch. if main_switch is not None and main_switch.getNumChildren() == 4: # Check if all display modes are available. for node in tuple(main_switch.getChildren()): node.removeAllChildren() diff --git a/src/Mod/BIM/ArchSectionPlane.py b/src/Mod/BIM/ArchSectionPlane.py index 84444756c0..a001e44a30 100644 --- a/src/Mod/BIM/ArchSectionPlane.py +++ b/src/Mod/BIM/ArchSectionPlane.py @@ -1173,7 +1173,7 @@ class _ViewProviderSectionPlane: actionToggleCutview = QtGui.QAction(QtGui.QIcon(":/icons/Draft_Edit.svg"), translate("Arch", "Toggle Cutview"), menu) - actionToggleCutview.triggered.connect(lambda f=self.toggleCutview, arg=vobj: f(arg)) + actionToggleCutview.triggered.connect(lambda: self.toggleCutview(vobj)) menu.addAction(actionToggleCutview) def edit(self): diff --git a/src/Mod/BIM/ArchSite.py b/src/Mod/BIM/ArchSite.py index 2d80e8f825..13693bb6ee 100644 --- a/src/Mod/BIM/ArchSite.py +++ b/src/Mod/BIM/ArchSite.py @@ -978,19 +978,12 @@ class _ViewProviderSite: """ from pivy import coin - - def find_node(parent, nodetype): - for i in range(parent.getNumChildren()): - if isinstance(parent.getChild(i), nodetype): - return parent.getChild(i) - return None + from draftutils import gui_utils if not hasattr(self, "terrain_switches"): - if vobj.RootNode.getNumChildren() > 2: - main_switch = find_node(vobj.RootNode, coin.SoSwitch) - if not main_switch: - return - if main_switch.getNumChildren() == 4: # Check if all display modes are available. + if vobj.RootNode.getNumChildren(): + main_switch = gui_utils.find_coin_node(vobj.RootNode, coin.SoSwitch) # The display mode switch. + if main_switch is not None and main_switch.getNumChildren() == 4: # Check if all display modes are available. self.terrain_switches = [] for node in tuple(main_switch.getChildren()): new_switch = coin.SoSwitch() diff --git a/src/Mod/BIM/ArchStructure.py b/src/Mod/BIM/ArchStructure.py index 14e9985359..72865ccbaa 100644 --- a/src/Mod/BIM/ArchStructure.py +++ b/src/Mod/BIM/ArchStructure.py @@ -627,10 +627,9 @@ class _CommandStructure: self.dents.form.hide() params.set_param_arch("StructurePreset",self.Profile) else: - p=elt[0]-1 # Presets indexes are 1-based - self.vLength.setText(FreeCAD.Units.Quantity(float(Presets[p][4]),FreeCAD.Units.Length).UserString) - self.vWidth.setText(FreeCAD.Units.Quantity(float(Presets[p][5]),FreeCAD.Units.Length).UserString) - self.Profile = Presets[p] + self.vLength.setText(FreeCAD.Units.Quantity(float(elt[4]),FreeCAD.Units.Length).UserString) + self.vWidth.setText(FreeCAD.Units.Quantity(float(elt[5]),FreeCAD.Units.Length).UserString) + self.Profile = elt params.set_param_arch("StructurePreset",";".join([str(i) for i in self.Profile])) def switchLH(self,bmode): diff --git a/src/Mod/BIM/Resources/ui/dialogImport.ui b/src/Mod/BIM/Resources/ui/dialogImport.ui index d94cf3ac3f..e4609879d8 100644 --- a/src/Mod/BIM/Resources/ui/dialogImport.ui +++ b/src/Mod/BIM/Resources/ui/dialogImport.ui @@ -113,6 +113,16 @@
+ + + + Preload IFC types that are connected to the objects. It is also possible to leave this setting disabled and double click later on the object to load the types + + + Preload types + + + diff --git a/src/Mod/BIM/Resources/ui/dialogProjectManager.ui b/src/Mod/BIM/Resources/ui/dialogProjectManager.ui index ffffe2703f..4a280670be 100644 --- a/src/Mod/BIM/Resources/ui/dialogProjectManager.ui +++ b/src/Mod/BIM/Resources/ui/dialogProjectManager.ui @@ -406,9 +406,6 @@ The main use of this building - - QComboBox::AdjustToMinimumContentsLength - 0 diff --git a/src/Mod/BIM/Resources/ui/preferencesNativeIFC.ui b/src/Mod/BIM/Resources/ui/preferencesNativeIFC.ui index 136249f741..43e9771878 100644 --- a/src/Mod/BIM/Resources/ui/preferencesNativeIFC.ui +++ b/src/Mod/BIM/Resources/ui/preferencesNativeIFC.ui @@ -129,6 +129,22 @@ + + + + Load all types automatically when opening an IFC file + + + Preload types + + + LoadTypes + + + Mod/NativeIFC + + + diff --git a/src/Mod/BIM/bimcommands/BimProfile.py b/src/Mod/BIM/bimcommands/BimProfile.py index 82434cbeeb..bf6f4ac618 100644 --- a/src/Mod/BIM/bimcommands/BimProfile.py +++ b/src/Mod/BIM/bimcommands/BimProfile.py @@ -77,7 +77,7 @@ class Arch_Profile: # categories box labelc = QtGui.QLabel(translate("Arch","Category")) self.vCategory = QtGui.QComboBox() - self.vCategory.addItems([" "] + self.Categories) + self.vCategory.addItems(self.Categories) grid.addWidget(labelc,1,0,1,1) grid.addWidget(self.vCategory,1,1,1,1) @@ -90,22 +90,32 @@ class Arch_Profile: grid.addWidget(labelp,2,0,1,1) grid.addWidget(self.vPresets,2,1,1,1) + # restore preset + categoryIdx = -1 + presetIdx = -1 + stored = params.get_param_arch("ProfilePreset") + if stored and ";" in stored and len(stored.split(";")) >= 3: + stored = stored.split(";") + if stored[1] in self.Categories: + categoryIdx = self.Categories.index(stored[1]) + self.setCategory(categoryIdx) + self.vCategory.setCurrentIndex(categoryIdx) + ps = [p[2] for p in self.pSelect] + if stored[2] in ps: + presetIdx = ps.index(stored[2]) + self.setPreset(presetIdx) + self.vPresets.setCurrentIndex(presetIdx) + if categoryIdx == -1: + self.setCategory(0) + self.vCategory.setCurrentIndex(0) + if presetIdx == -1: + self.setPreset(0) + self.vPresets.setCurrentIndex(0) + # connect slots self.vCategory.currentIndexChanged.connect(self.setCategory) self.vPresets.currentIndexChanged.connect(self.setPreset) - # restore preset - stored = params.get_param_arch("StructurePreset") - if stored: - if ";" in stored: - stored = stored.split(";") - if len(stored) >= 3: - if stored[1] in self.Categories: - self.vCategory.setCurrentIndex(1+self.Categories.index(stored[1])) - self.setCategory(1+self.Categories.index(stored[1])) - ps = [p[2] for p in self.pSelect] - if stored[2] in ps: - self.vPresets.setCurrentIndex(ps.index(stored[2])) return w def getPoint(self,point=None,obj=None): @@ -134,32 +144,23 @@ class Arch_Profile: from draftutils import params self.vPresets.clear() - if i == 0: - self.pSelect = [None] - self.vPresets.addItems([" "]) - params.set_param_arch("StructurePreset","") - else: - self.pSelect = [p for p in self.Presets if p[1] == self.Categories[i-1]] - fpresets = [] - for p in self.pSelect: - f = FreeCAD.Units.Quantity(p[4],FreeCAD.Units.Length).getUserPreferred() - d = params.get_param("Decimals",path="Units") - s1 = str(round(p[4]/f[1],d)) - s2 = str(round(p[5]/f[1],d)) - s3 = str(f[2]) - fpresets.append(p[2]+" ("+s1+"x"+s2+s3+")") - self.vPresets.addItems(fpresets) - self.setPreset(0) + self.pSelect = [p for p in self.Presets if p[1] == self.Categories[i]] + fpresets = [] + for p in self.pSelect: + f = FreeCAD.Units.Quantity(p[4],FreeCAD.Units.Length).getUserPreferred() + d = params.get_param("Decimals",path="Units") + s1 = str(round(p[4]/f[1],d)) + s2 = str(round(p[5]/f[1],d)) + s3 = str(f[2]) + fpresets.append(p[2]+" ("+s1+"x"+s2+s3+")") + self.vPresets.addItems(fpresets) + self.setPreset(0) def setPreset(self,i): from draftutils import params - self.Profile = None - elt = self.pSelect[i] - if elt: - p=elt[0]-1 # Presets indexes are 1-based - self.Profile = self.Presets[p] - params.set_param_arch("StructurePreset",";".join([str(i) for i in self.Profile])) + self.Profile = self.pSelect[i] + params.set_param_arch("ProfilePreset",";".join([str(i) for i in self.Profile])) FreeCADGui.addCommand('Arch_Profile',Arch_Profile()) diff --git a/src/Mod/BIM/bimcommands/BimWall.py b/src/Mod/BIM/bimcommands/BimWall.py index 35c91d5938..bfa3293d48 100644 --- a/src/Mod/BIM/bimcommands/BimWall.py +++ b/src/Mod/BIM/bimcommands/BimWall.py @@ -114,7 +114,7 @@ class Arch_Wall: self.tracker = DraftTrackers.boxTracker() FreeCADGui.Snapper.getPoint(callback=self.getPoint, extradlg=self.taskbox(), - title=translate("Arch","First point of wall")+":") + title=translate("Arch", "First point of wall")) FreeCADGui.draftToolBar.continueCmd.show() def getPoint(self,point=None,obj=None): @@ -132,8 +132,6 @@ class Arch_Wall: """ import Draft - import Part - import Arch import ArchWall from draftutils import gui_utils if obj: @@ -154,40 +152,36 @@ class Arch_Wall: callback=self.getPoint, movecallback=self.update, extradlg=self.taskbox(), - title=translate("Arch","Next point")+":",mode="line") + title=translate("Arch", "Next point"), + mode="line") elif len(self.points) == 2: FreeCAD.activeDraftCommand = None FreeCADGui.Snapper.off() self.tracker.off() - l = Part.LineSegment(self.wp.get_local_coords(self.points[0]), - self.wp.get_local_coords(self.points[1])) - self.doc.openTransaction(translate("Arch","Create Wall")) - FreeCADGui.addModule("Arch") - FreeCADGui.doCommand('import Part') - FreeCADGui.doCommand('trace=Part.LineSegment(FreeCAD.'+str(l.StartPoint)+',FreeCAD.'+str(l.EndPoint)+')') - if not self.existing: - # no existing wall snapped, just add a default wall - self.addDefault() - else: - if self.JOIN_WALLS_SKETCHES: - # join existing subwalls first if possible, then add the new one - w = Arch.joinWalls(self.existing) - if w: - if ArchWall.areSameWallTypes([w,self]): - FreeCADGui.doCommand('FreeCAD.ActiveDocument.'+w.Name+'.Base.addGeometry(trace)') - else: - # if not possible, add new wall as addition to the existing one - self.addDefault() - if self.AUTOJOIN: - FreeCADGui.doCommand('Arch.addComponents(FreeCAD.ActiveDocument.'+self.doc.Objects[-1].Name+',FreeCAD.ActiveDocument.'+w.Name+')') - else: - self.addDefault() - else: - # add new wall as addition to the first existing one - self.addDefault() - if self.AUTOJOIN: - FreeCADGui.doCommand('Arch.addComponents(FreeCAD.ActiveDocument.'+self.doc.Objects[-1].Name+',FreeCAD.ActiveDocument.'+self.existing[0].Name+')') + + self.doc.openTransaction(translate("Arch", "Create Wall")) + + # Some code in gui_utils.autogroup requires a wall shape to determine + # the target group. We therefore need to create a wall first. + self.addDefault() + wall = self.doc.Objects[-1] + wallGrp = wall.getParentGroup() + + if (self.JOIN_WALLS_SKETCHES or self.AUTOJOIN) \ + and self.existing \ + and self.existing[-1].getParentGroup() == wallGrp: + oldWall = self.existing[-1] + if self.JOIN_WALLS_SKETCHES and ArchWall.areSameWallTypes([wall, oldWall]): + FreeCADGui.doCommand( + "Arch.joinWalls([wall, doc." + oldWall.Name + "], " + + "delete=True, deletebase=True)" + ) + elif self.AUTOJOIN: + if wallGrp is not None: + FreeCADGui.doCommand("wall.getParentGroup().removeObject(wall)") + FreeCADGui.doCommand("Arch.addComponents(wall, doc." + oldWall.Name + ")") + self.doc.commitTransaction() self.doc.recompute() # gui_utils.end_all_events() # Causes a crash on Linux. @@ -200,36 +194,48 @@ class Arch_Wall: Used solely by _CommandWall.getPoint() when the interactive mode has selected two points. - - Relies on the assumption that FreeCADGui.doCommand() has already - created a Part.LineSegment assigned as the variable "trace" """ - from draftutils import params + + sta = self.wp.get_local_coords(self.points[0]) + end = self.wp.get_local_coords(self.points[1]) + + FreeCADGui.doCommand("import Part") FreeCADGui.addModule("Draft") + FreeCADGui.addModule("Arch") FreeCADGui.addModule("WorkingPlane") + FreeCADGui.doCommand("doc = FreeCAD.ActiveDocument") FreeCADGui.doCommand("wp = WorkingPlane.get_working_plane()") + FreeCADGui.doCommand( + "trace = Part.LineSegment(FreeCAD." + str(sta) + ", FreeCAD." + str(end) + ")" + ) if params.get_param_arch("WallSketches"): # Use ArchSketch if SketchArch add-on is present try: import ArchSketchObject - FreeCADGui.doCommand('import ArchSketchObject') - FreeCADGui.doCommand('base=ArchSketchObject.makeArchSketch()') + FreeCADGui.doCommand("import ArchSketchObject") + FreeCADGui.doCommand("base = ArchSketchObject.makeArchSketch()") except: - FreeCADGui.doCommand('base=FreeCAD.ActiveDocument.addObject("Sketcher::SketchObject","WallTrace")') - FreeCADGui.doCommand('base.Placement = wp.get_placement()') - FreeCADGui.doCommand('base.addGeometry(trace)') + FreeCADGui.doCommand( + "base = doc.addObject(\"Sketcher::SketchObject\", \"WallTrace\")" + ) + FreeCADGui.doCommand("base.Placement = wp.get_placement()") + FreeCADGui.doCommand("base.addGeometry(trace)") else: - FreeCADGui.doCommand('base=Draft.make_line(trace)') + FreeCADGui.doCommand("base = Draft.make_line(trace)") # The created line should not stay selected as this causes an issue in continue mode. # Two walls would then be created based on the same line. FreeCADGui.Selection.clearSelection() - FreeCADGui.doCommand('base.Placement = wp.get_placement()') - FreeCADGui.doCommand('FreeCAD.ActiveDocument.recompute()') - FreeCADGui.doCommand('wall = Arch.makeWall(base,width='+str(self.Width)+',height='+str(self.Height)+',align="'+str(self.Align)+'")') - FreeCADGui.doCommand('wall.Normal = wp.axis') + FreeCADGui.doCommand("base.Placement = wp.get_placement()") + FreeCADGui.doCommand("doc.recompute()") + FreeCADGui.doCommand( + "wall = Arch.makeWall(base, width=" + str(self.Width) + + ", height=" + str(self.Height) + ", align=\"" + str(self.Align) + "\")" + ) + FreeCADGui.doCommand("wall.Normal = wp.axis") if self.MultiMat: - FreeCADGui.doCommand("wall.Material = FreeCAD.ActiveDocument."+self.MultiMat.Name) + FreeCADGui.doCommand("wall.Material = doc." + self.MultiMat.Name) + FreeCADGui.doCommand("doc.recompute()") # required as some autogroup code requires the wall shape FreeCADGui.doCommand("Draft.autogroup(wall)") def update(self,point,info): diff --git a/src/Mod/BIM/nativeifc/ifc_import.py b/src/Mod/BIM/nativeifc/ifc_import.py index 5209401baa..5f539c1cf9 100644 --- a/src/Mod/BIM/nativeifc/ifc_import.py +++ b/src/Mod/BIM/nativeifc/ifc_import.py @@ -32,6 +32,7 @@ from . import ifc_psets from . import ifc_materials from . import ifc_layers from . import ifc_status +from . import ifc_types if FreeCAD.GuiUp: import FreeCADGui @@ -101,6 +102,8 @@ def insert( ifc_layers.load_layers(prj_obj) if PARAMS.GetBool("LoadPsets", False): ifc_psets.load_psets(prj_obj) + if PARAMS.GetBool("LoadTypes", False): + ifc_types.load_types(prj_obj) document.recompute() # print a reference to the IFC file on the console if FreeCAD.GuiUp and PARAMS.GetBool("IfcFileToConsole", False): @@ -133,6 +136,7 @@ def get_options(strategy=None, shapemode=None, switchwb=None, silent=False): """ psets = PARAMS.GetBool("LoadPsets", False) + types = PARAMS.GetBool("LoadTypes", False) materials = PARAMS.GetBool("LoadMaterials", False) layers = PARAMS.GetBool("LoadLayers", False) singledoc = PARAMS.GetBool("SingleDoc", False) @@ -155,6 +159,7 @@ def get_options(strategy=None, shapemode=None, switchwb=None, silent=False): dlg.checkSwitchWB.setChecked(switchwb) dlg.checkAskAgain.setChecked(ask) dlg.checkLoadPsets.setChecked(psets) + dlg.checkLoadTypes.setChecked(types) dlg.checkLoadMaterials.setChecked(materials) dlg.checkLoadLayers.setChecked(layers) dlg.comboSingleDoc.setCurrentIndex(1 - int(singledoc)) @@ -166,6 +171,7 @@ def get_options(strategy=None, shapemode=None, switchwb=None, silent=False): switchwb = dlg.checkSwitchWB.isChecked() ask = dlg.checkAskAgain.isChecked() psets = dlg.checkLoadPsets.isChecked() + types = dlg.checkLoadTypes.isChecked() materials = dlg.checkLoadMaterials.isChecked() layers = dlg.checkLoadLayers.isChecked() singledoc = dlg.comboSingleDoc.currentIndex() @@ -174,6 +180,7 @@ def get_options(strategy=None, shapemode=None, switchwb=None, silent=False): PARAMS.SetBool("SwitchWB", switchwb) PARAMS.SetBool("AskAgain", ask) PARAMS.SetBool("LoadPsets", psets) + PARAMS.SetBool("LoadTypes", types) PARAMS.SetBool("LoadMaterials", materials) PARAMS.SetBool("LoadLayers", layers) PARAMS.SetBool("SingleDoc", bool(1 - singledoc)) diff --git a/src/Mod/BIM/nativeifc/ifc_tools.py b/src/Mod/BIM/nativeifc/ifc_tools.py index 8bc16126b4..619c58419f 100644 --- a/src/Mod/BIM/nativeifc/ifc_tools.py +++ b/src/Mod/BIM/nativeifc/ifc_tools.py @@ -350,6 +350,7 @@ def create_children( ] for window in windows: subresult.extend(create_child(child, window)) + if recursive: subresult.extend( create_children( diff --git a/src/Mod/BIM/nativeifc/ifc_types.py b/src/Mod/BIM/nativeifc/ifc_types.py index 7967c471a9..ce95c8222d 100644 --- a/src/Mod/BIM/nativeifc/ifc_types.py +++ b/src/Mod/BIM/nativeifc/ifc_types.py @@ -51,6 +51,34 @@ def show_type(obj): obj.Type = typeobj +def load_types(prj_obj): + """ + Loads IFC types for all objects in the project, used during + import of IFC files. + prj_obj is the project object, either a document or a document object. + """ + + def process_object(obj): + """Recursively process an object and its children""" + # Check if this object has IFC data and can have types + if hasattr(obj, 'StepId') and obj.StepId: + show_type(obj) + + # Process children recursively + if hasattr(obj, 'Group'): + for child in obj.Group: + process_object(child) + + if isinstance(prj_obj, FreeCAD.DocumentObject): + # Handle document object case + process_object(prj_obj) + else: + # Handle document case - process all IFC objects in the document + for obj in prj_obj.Objects: + if hasattr(obj, 'StepId') and obj.StepId: + show_type(obj) + + def is_typable(obj): """Checks if an object can become a type""" @@ -87,10 +115,9 @@ def convert_to_type(obj, keep_object=False): original_text = dlg.label.text() dlg.label.setText(original_text.replace("%1", obj.Class+"Type")) - + # Set the initial state of the checkbox from the "always keep" preference dlg.checkKeepObject.setChecked(always_keep) - result = dlg.exec_() if not result: return diff --git a/src/Mod/CAM/Path/Op/Gui/Array.py b/src/Mod/CAM/Path/Op/Gui/Array.py index 8a33d58280..956bb88042 100644 --- a/src/Mod/CAM/Path/Op/Gui/Array.py +++ b/src/Mod/CAM/Path/Op/Gui/Array.py @@ -24,11 +24,10 @@ import FreeCAD import FreeCADGui import Path import Path.Op.Base as PathOp -import PathScripts import PathScripts.PathUtils as PathUtils +import Path.Base.Util as PathUtil from Path.Dressup.Utils import toolController from PySide import QtCore -from PySide import QtGui import random @@ -154,8 +153,6 @@ class ObjectArray: self.setEditorModes(obj) obj.Proxy = self - self.FirstRun = True - def dumps(self): return None @@ -223,42 +220,21 @@ class ObjectArray: self.setEditorModes(obj) - self.FirstRun = True - def execute(self, obj): - if FreeCAD.GuiUp and self.FirstRun: - self.FirstRun = False - QtGui.QMessageBox.warning( - None, - QT_TRANSLATE_NOOP("CAM_ArrayOp", "Operation is deprecated"), - QT_TRANSLATE_NOOP( - "CAM_ArrayOp", - ( - "CAM -> Path Modification -> Array operation is deprecated " - "and will be removed in future FreeCAD versions.\n\n" - "Please use CAM -> Path Dressup -> Array instead.\n\n" - "DO NOT USE CURRENT ARRAY OPERATION WHEN MACHINING WITH COOLANT!\n" - "Due to a bug - coolant will not be enabled for array paths." - ), - ), - ) # backwards compatibility for PathArrays created before support for multiple bases if isinstance(obj.Base, list): base = obj.Base else: base = [obj.Base] - if len(base) == 0: + # Do not generate paths and clear current Path data + # if operation not Active or no base operations or operations not compatible + if not obj.Active or len(base) == 0 or not self.isBaseCompatible(obj): + obj.Path = Path.Path() return obj.ToolController = toolController(base[0]) - # Do not generate paths and clear current Path data if operation not - if not obj.Active: - if obj.Path: - obj.Path = Path.Path() - return - # use seed if specified, otherwise default to object name for consistency during recomputes seed = obj.JitterSeed or obj.Name @@ -280,6 +256,37 @@ class ObjectArray: obj.Path = pa.getPath() obj.CycleTime = PathOp.getCycleTimeEstimate(obj) + def isBaseCompatible(self, obj): + if not obj.Base: + return False + tcs = [] + cms = [] + for sel in obj.Base: + if not sel.isDerivedFrom("Path::Feature"): + return False + tcs.append(toolController(sel)) + cms.append(PathUtil.coolantModeForOp(sel)) + + if tcs == {None} or len(set(tcs)) > 1: + Path.Log.warning( + translate( + "PathArray", + "Arrays of toolpaths having different tool controllers or tool controller not selected.", + ) + ) + return False + + if set(cms) != {"None"}: + Path.Log.warning( + translate( + "PathArray", + "Arrays not compatible with coolant modes.", + ) + ) + return False + + return True + class PathArray: """class PathArray ... @@ -337,10 +344,6 @@ class PathArray: """getPath() ... Call this method on an instance of the class to generate and return path data for the requested path array.""" - if len(self.baseList) == 0: - Path.Log.error(translate("PathArray", "No base objects for PathArray.")) - return None - base = self.baseList for b in base: if not b.isDerivedFrom("Path::Feature"): @@ -352,15 +355,6 @@ class PathArray: if not b_tool_controller: return - if b_tool_controller != toolController(base[0]): - # this may be important if Job output is split by tool controller - Path.Log.warning( - translate( - "PathArray", - "Arrays of toolpaths having different tool controllers are handled according to the tool controller of the first path.", - ) - ) - # build copies output = "" random.seed(self.seed) @@ -486,14 +480,32 @@ class CommandPathArray: return { "Pixmap": "CAM_Array", "MenuText": QT_TRANSLATE_NOOP("CAM_Array", "Array"), - "ToolTip": QT_TRANSLATE_NOOP("CAM_Array", "Creates an array from selected toolpath(s)"), + "ToolTip": QT_TRANSLATE_NOOP( + "CAM_Array", + "Creates an array from selected toolpath(s)\nwith identical tool controllers and without coolant", + ), } def IsActive(self): - selections = [ - sel.isDerivedFrom("Path::Feature") for sel in FreeCADGui.Selection.getSelection() - ] - return selections and all(selections) + selection = FreeCADGui.Selection.getSelection() + if not selection: + return False + tcs = [] + for sel in selection: + if not sel.isDerivedFrom("Path::Feature"): + return False + tc = toolController(sel) + if tc: + # Active only for operations with identical tool controller + tcs.append(tc) + if len(set(tcs)) != 1: + return False + else: + return False + if PathUtil.coolantModeForOp(sel) != "None": + # Active only for operations without cooling + return False + return True def Activated(self): diff --git a/src/Mod/Draft/draftmake/make_sketch.py b/src/Mod/Draft/draftmake/make_sketch.py index ea055e9924..3f958bbccc 100644 --- a/src/Mod/Draft/draftmake/make_sketch.py +++ b/src/Mod/Draft/draftmake/make_sketch.py @@ -78,11 +78,6 @@ def make_sketch(objects_list, autoconstraints=False, addTo=None, import Part from Sketcher import Constraint - if App.GuiUp: - v_dir = gui_utils.get_3d_view().getViewDirection() - else: - v_dir = App.Vector(0, 0, -1) - # lists to accumulate shapes with defined normal and undefined normal shape_norm_yes = list() shape_norm_no = list() @@ -127,21 +122,20 @@ def make_sketch(objects_list, autoconstraints=False, addTo=None, # suppose all geometries are straight lines or points points = [vertex.Point for shape in shapes_list for vertex in shape.Vertexes] if len(points) >= 2: - poly = Part.makePolygon(points) - if not DraftGeomUtils.is_planar(poly, tol): - App.Console.PrintError(translate("draft", - "All Shapes must be coplanar") + "\n") - return None - normal = DraftGeomUtils.get_normal(poly, tol) - if not normal: - # all points aligned - poly_dir = poly.Edges[0].Curve.Direction - normal = (v_dir - v_dir.dot(poly_dir)*poly_dir).normalize() - normal = normal.negative() + try: + poly = Part.makePolygon(points) + except Part.OCCError: + # all points coincide + normal = App.Vector(0, 0, 1) + else: + if not DraftGeomUtils.is_planar(poly, tol): + App.Console.PrintError(translate("draft", + "All Shapes must be coplanar") + "\n") + return None + normal = DraftGeomUtils.get_shape_normal(poly) else: # only one point - normal = v_dir.negative() - + normal = App.Vector(0, 0, 1) if addTo: nobj = addTo diff --git a/src/Mod/Draft/draftutils/gui_utils.py b/src/Mod/Draft/draftutils/gui_utils.py index 698f5e9481..b6ab35aafc 100644 --- a/src/Mod/Draft/draftutils/gui_utils.py +++ b/src/Mod/Draft/draftutils/gui_utils.py @@ -120,71 +120,66 @@ def autogroup(obj): return # autogroup code - active_group = None if Gui.draftToolBar.autogroup is not None: active_group = App.ActiveDocument.getObject(Gui.draftToolBar.autogroup) - if active_group: - gr = active_group.Group - if not obj in gr: - gr.append(obj) - active_group.Group = gr + if obj in active_group.InListRecursive: + return + if not obj in active_group.Group: + active_group.Group += [obj] - if Gui.ActiveDocument.ActiveView.getActiveObject("NativeIFC") is not None: + elif Gui.ActiveDocument.ActiveView.getActiveObject("NativeIFC") is not None: # NativeIFC handling try: from nativeifc import ifc_tools parent = Gui.ActiveDocument.ActiveView.getActiveObject("NativeIFC") - if parent != active_group: - ifc_tools.aggregate(obj, parent) + ifc_tools.aggregate(obj, parent) except: pass elif Gui.ActiveDocument.ActiveView.getActiveObject("Arch") is not None: # add object to active Arch Container active_arch_obj = Gui.ActiveDocument.ActiveView.getActiveObject("Arch") - if active_arch_obj != active_group: - if obj in active_arch_obj.InListRecursive: - # do not autogroup if obj points to active_arch_obj to prevent cyclic references - return - active_arch_obj.addObject(obj) + if obj in active_arch_obj.InListRecursive: + # do not autogroup if obj points to active_arch_obj to prevent cyclic references + return + active_arch_obj.addObject(obj) elif Gui.ActiveDocument.ActiveView.getActiveObject("part") is not None: # add object to active part and change it's placement accordingly # so object does not jump to different position, works with App::Link # if not scaled. Modified accordingly to realthunder suggestions active_part, parent, sub = Gui.ActiveDocument.ActiveView.getActiveObject("part", False) - if active_part != active_group: - if obj in active_part.InListRecursive: - # do not autogroup if obj points to active_part to prevent cyclic references - return - matrix = parent.getSubObject(sub, retType=4) - if matrix.hasScale() == App.ScaleType.Uniform: - err = translate("draft", - "Unable to insert new object into " - "a scaled part") - App.Console.PrintMessage(err) - return - inverse_placement = App.Placement(matrix.inverse()) - if utils.get_type(obj) == 'Point': - point_vector = App.Vector(obj.X, obj.Y, obj.Z) - real_point = inverse_placement.multVec(point_vector) - obj.X = real_point.x - obj.Y = real_point.y - obj.Z = real_point.z - elif utils.get_type(obj) in ["Dimension", "LinearDimension"]: - obj.Start = inverse_placement.multVec(obj.Start) - obj.End = inverse_placement.multVec(obj.End) - obj.Dimline = inverse_placement.multVec(obj.Dimline) - obj.Normal = inverse_placement.Rotation.multVec(obj.Normal) - obj.Direction = inverse_placement.Rotation.multVec(obj.Direction) - elif utils.get_type(obj) in ["Label"]: - obj.Placement = App.Placement(inverse_placement.multiply(obj.Placement)) - obj.TargetPoint = inverse_placement.multVec(obj.TargetPoint) - elif hasattr(obj,"Placement"): - # every object that have a placement is processed here - obj.Placement = App.Placement(inverse_placement.multiply(obj.Placement)) + if obj in active_part.InListRecursive: + # do not autogroup if obj points to active_part to prevent cyclic references + return + matrix = parent.getSubObject(sub, retType=4) + if matrix.hasScale() == App.ScaleType.Uniform: + err = translate("draft", + "Unable to insert new object into " + "a scaled part") + App.Console.PrintMessage(err) + return + inverse_placement = App.Placement(matrix.inverse()) + if utils.get_type(obj) == 'Point': + point_vector = App.Vector(obj.X, obj.Y, obj.Z) + real_point = inverse_placement.multVec(point_vector) + obj.X = real_point.x + obj.Y = real_point.y + obj.Z = real_point.z + elif utils.get_type(obj) in ["Dimension", "LinearDimension"]: + obj.Start = inverse_placement.multVec(obj.Start) + obj.End = inverse_placement.multVec(obj.End) + obj.Dimline = inverse_placement.multVec(obj.Dimline) + obj.Normal = inverse_placement.Rotation.multVec(obj.Normal) + obj.Direction = inverse_placement.Rotation.multVec(obj.Direction) + elif utils.get_type(obj) in ["Label"]: + obj.Placement = App.Placement(inverse_placement.multiply(obj.Placement)) + obj.TargetPoint = inverse_placement.multVec(obj.TargetPoint) + elif hasattr(obj,"Placement"): + # every object that have a placement is processed here + obj.Placement = App.Placement(inverse_placement.multiply(obj.Placement)) - active_part.addObject(obj) + active_part.addObject(obj) def dim_symbol(symbol=None, invert=False): diff --git a/src/Mod/Draft/draftutils/params.py b/src/Mod/Draft/draftutils/params.py index ad49b2b7bf..f2046e5c52 100644 --- a/src/Mod/Draft/draftutils/params.py +++ b/src/Mod/Draft/draftutils/params.py @@ -558,6 +558,7 @@ def _get_param_dictionary(): "PrecastHoleSpacing": ("float", 0.0), "PrecastRiser": ("float", 0.0), "PrecastTread": ("float", 0.0), + "ProfilePreset": ("string", ""), "ScheduleColumnWidth0": ("int", 100), "ScheduleColumnWidth1": ("int", 100), "ScheduleColumnWidth2": ("int", 50), diff --git a/src/Mod/Fem/Gui/Resources/ui/ElementGeometry1D.ui b/src/Mod/Fem/Gui/Resources/ui/ElementGeometry1D.ui index b28f2951ff..1bc29e2a2d 100644 --- a/src/Mod/Fem/Gui/Resources/ui/ElementGeometry1D.ui +++ b/src/Mod/Fem/Gui/Resources/ui/ElementGeometry1D.ui @@ -274,14 +274,14 @@ QFormLayout::AllNonFixedFieldsGrow - + - Height + Width - + Qt::AlignLeft|Qt::AlignTrailing|Qt::AlignVCenter @@ -306,14 +306,14 @@ - + - Width + Height - + Qt::AlignLeft|Qt::AlignTrailing|Qt::AlignVCenter diff --git a/src/Mod/Part/App/PartFeatures.cpp b/src/Mod/Part/App/PartFeatures.cpp index dab9d9219d..be5de959f7 100644 --- a/src/Mod/Part/App/PartFeatures.cpp +++ b/src/Mod/Part/App/PartFeatures.cpp @@ -177,7 +177,7 @@ Loft::Loft() { ADD_PROPERTY_TYPE(Sections, (nullptr), "Loft", App::Prop_None, "List of sections"); Sections.setSize(0); - ADD_PROPERTY_TYPE(Solid, (false), "Loft", App::Prop_None, "Create solid"); + ADD_PROPERTY_TYPE(Solid, (true), "Loft", App::Prop_None, "Create solid"); ADD_PROPERTY_TYPE(Ruled, (false), "Loft", App::Prop_None, "Ruled surface"); ADD_PROPERTY_TYPE(Closed, (false), "Loft", App::Prop_None, "Close Last to First Profile"); ADD_PROPERTY_TYPE(MaxDegree, (5), "Loft", App::Prop_None, "Maximum Degree"); @@ -257,7 +257,7 @@ Sweep::Sweep() ADD_PROPERTY_TYPE(Sections, (nullptr), "Sweep", App::Prop_None, "List of sections"); Sections.setSize(0); ADD_PROPERTY_TYPE(Spine, (nullptr), "Sweep", App::Prop_None, "Path to sweep along"); - ADD_PROPERTY_TYPE(Solid, (false), "Sweep", App::Prop_None, "Create solid"); + ADD_PROPERTY_TYPE(Solid, (true), "Sweep", App::Prop_None, "Create solid"); ADD_PROPERTY_TYPE(Frenet, (true), "Sweep", App::Prop_None, "Frenet"); ADD_PROPERTY_TYPE(Transition, (long(1)), "Sweep", App::Prop_None, "Transition mode"); ADD_PROPERTY_TYPE(Linearize,(false), "Sweep", App::Prop_None, diff --git a/src/Mod/Part/Gui/TaskLoft.ui b/src/Mod/Part/Gui/TaskLoft.ui index d47c1ebff1..2b9178aa95 100644 --- a/src/Mod/Part/Gui/TaskLoft.ui +++ b/src/Mod/Part/Gui/TaskLoft.ui @@ -22,6 +22,9 @@ Create solid + + true + diff --git a/src/Mod/Part/Gui/TaskSweep.ui b/src/Mod/Part/Gui/TaskSweep.ui index 3b2ee55c37..cc563b59d9 100644 --- a/src/Mod/Part/Gui/TaskSweep.ui +++ b/src/Mod/Part/Gui/TaskSweep.ui @@ -52,6 +52,9 @@ Create solid + + true + diff --git a/src/Mod/Sandbox/App/AppSandbox.cpp b/src/Mod/Sandbox/App/AppSandbox.cpp index 5a6adf4c7a..f74381bb60 100644 --- a/src/Mod/Sandbox/App/AppSandbox.cpp +++ b/src/Mod/Sandbox/App/AppSandbox.cpp @@ -253,6 +253,6 @@ PyMOD_INIT_FUNC(Sandbox) // the following constructor call registers our extension module // with the Python runtime system PyObject* mod = Sandbox::initModule(); - Base::Console().log("Loading Sandbox module... done\n"); + Base::Console().log("Loading Sandbox module… done\n"); PyMOD_Return(mod); } diff --git a/src/Mod/Sandbox/App/DocumentProtectorPy.cpp b/src/Mod/Sandbox/App/DocumentProtectorPy.cpp index c74d0977e2..af672fdac5 100644 --- a/src/Mod/Sandbox/App/DocumentProtectorPy.cpp +++ b/src/Mod/Sandbox/App/DocumentProtectorPy.cpp @@ -136,7 +136,7 @@ Py::Object DocumentProtectorPy::addObject(const Py::Tuple& args) if (!obj) { std::string s; std::ostringstream s_out; - s_out << "Couldn't create an object of type '" << type << "'"; + s_out << "Could not create an object of type '" << type << "'"; throw Py::RuntimeError(s_out.str()); } //return Py::asObject(obj->getPyObject()); diff --git a/src/Mod/Sandbox/App/DocumentThread.cpp b/src/Mod/Sandbox/App/DocumentThread.cpp index b8df4ce511..f3756d8df5 100644 --- a/src/Mod/Sandbox/App/DocumentThread.cpp +++ b/src/Mod/Sandbox/App/DocumentThread.cpp @@ -76,7 +76,7 @@ void WorkerThread::run() #else int max = 100000000; #endif - Base::SequencerLauncher seq("Do something meaningful...", max); + Base::SequencerLauncher seq("Do something meaningful…", max); double val=0; for (int i=0; i mesh_future = QtConcurrent::mapped (mesh_groups, boost::bind(&MeshTestJob::run, &meshJob, bp::_1)); @@ -877,10 +877,10 @@ CmdSandboxMeshTestRef::CmdSandboxMeshTestRef() { sAppModule = "Sandbox"; sGroup = QT_TR_NOOP("Sandbox"); - sMenuText = QT_TR_NOOP("Test mesh reference"); - sToolTipText = QT_TR_NOOP("Sandbox Test function"); + sMenuText = QT_TR_NOOP("Test Mesh Reference"); + sToolTipText = QT_TR_NOOP("Runs a sandbox test function"); sWhatsThis = "Sandbox_MeshTestRef"; - sStatusTip = QT_TR_NOOP("Sandbox Test function"); + sStatusTip = sToolTipText; } void CmdSandboxMeshTestRef::activated(int) @@ -924,8 +924,8 @@ CmdTestGrabWidget::CmdTestGrabWidget() : Command("Std_GrabWidget") { sGroup = "Standard-Test"; - sMenuText = "Grab widget"; - sToolTipText = "Grab widget"; + sMenuText = "Grab Widget"; + sToolTipText = "Grabs a widget"; sWhatsThis = "Std_GrabWidget"; sStatusTip = sToolTipText; } @@ -1045,7 +1045,7 @@ CmdTestImageNode::CmdTestImageNode() : Command("Std_ImageNode") { sGroup = "Standard-Test"; - sMenuText = "SoImage node"; + sMenuText = "SoImage Node"; sToolTipText = "SoImage node"; sWhatsThis = "Std_ImageNode"; sStatusTip = sToolTipText; @@ -1109,7 +1109,7 @@ CmdTestGDIWidget::CmdTestGDIWidget() : Command("Sandbox_GDIWidget") { sGroup = "Standard-Test"; - sMenuText = "GDI widget"; + sMenuText = "GDI Widget"; sToolTipText = "GDI widget"; sWhatsThis = "Sandbox_GDIWidget"; sStatusTip = sToolTipText; @@ -1135,7 +1135,7 @@ CmdTestRedirectPaint::CmdTestRedirectPaint() : Command("Sandbox_RedirectPaint") { sGroup = "Standard-Test"; - sMenuText = "Redirect paint"; + sMenuText = "Redirect Paint"; sToolTipText = "Redirect paint"; sWhatsThis = "Sandbox_RedirectPaint"; sStatusTip = sToolTipText; @@ -1165,7 +1165,7 @@ CmdTestCryptographicHash::CmdTestCryptographicHash() { sGroup = "Standard-Test"; sMenuText = "Cryptographic Hash"; - sToolTipText = "Cryptographic Hash"; + sToolTipText = "Cryptographic hash"; sWhatsThis = "Sandbox_CryptographicHash"; sStatusTip = sToolTipText; } @@ -1186,7 +1186,7 @@ CmdTestWidgetShape::CmdTestWidgetShape() : Command("Sandbox_WidgetShape") { sGroup = "Standard-Test"; - sMenuText = "Widget shape"; + sMenuText = "Widget Shape"; sToolTipText = "Widget shape"; sWhatsThis = "Sandbox_WidgetShape"; sStatusTip = sToolTipText; @@ -1210,10 +1210,10 @@ CmdMengerSponge::CmdMengerSponge() { sAppModule = "Sandbox"; sGroup = QT_TR_NOOP("Sandbox"); - sMenuText = QT_TR_NOOP("Menger sponge"); + sMenuText = QT_TR_NOOP("Menger Sponge"); sToolTipText = QT_TR_NOOP("Menger sponge"); sWhatsThis = "Sandbox_MengerSponge"; - sStatusTip = QT_TR_NOOP("Menger sponge"); + sStatusTip = sToolTipText; } struct Param { @@ -1341,7 +1341,7 @@ void CmdMengerSponge::activated(int) return; int ret = QMessageBox::question(Gui::getMainWindow(), QStringLiteral("Parallel"), - QStringLiteral("Do you want to run this in a thread pool?"), + QStringLiteral("Run this in a thread pool?"), QMessageBox::Yes|QMessageBox::No); bool parallel=(ret == QMessageBox::Yes); float x0=0,y0=0,z0=0; @@ -1379,9 +1379,9 @@ CmdTestGraphicsView::CmdTestGraphicsView() : Command("Std_TestGraphicsView") { sGroup = QT_TR_NOOP("Standard-Test"); - sMenuText = QT_TR_NOOP("Create new graphics view"); + sMenuText = QT_TR_NOOP("New Graphics View"); sToolTipText= QT_TR_NOOP("Creates a new view window for the active document"); - sStatusTip = QT_TR_NOOP("Creates a new view window for the active document"); + sStatusTip = sToolTipText; } void CmdTestGraphicsView::activated(int) @@ -1407,7 +1407,7 @@ CmdTestTaskBox::CmdTestTaskBox() : Command("Std_TestTaskBox") { sGroup = "Standard-Test"; - sMenuText = "Task box"; + sMenuText = "Task Box"; sToolTipText = "Task box"; sWhatsThis = "Std_TestTaskBox"; sStatusTip = sToolTipText; diff --git a/src/Mod/Sketcher/App/SketchObject.cpp b/src/Mod/Sketcher/App/SketchObject.cpp index b1a7433052..4b5d53e1cd 100644 --- a/src/Mod/Sketcher/App/SketchObject.cpp +++ b/src/Mod/Sketcher/App/SketchObject.cpp @@ -8632,9 +8632,6 @@ void SketchObject::rebuildExternalGeometry(std::optional extToAdd auto SubElements = ExternalGeometry.getSubValues(); assert(externalGeoRef.size() == Objects.size()); auto keys = externalGeoRef; - if (Types.size() != Objects.size()) { - Types.resize(Objects.size(), 0); - } // re-check for any missing geometry element. The code here has a side // effect that the linked external geometry will continue to work even if @@ -8695,6 +8692,10 @@ void SketchObject::rebuildExternalGeometry(std::optional extToAdd BRepBuilderAPI_MakeFace mkFace(sketchPlane); TopoDS_Shape aProjFace = mkFace.Shape(); + if (Types.size() != Objects.size()) { + Types.resize(Objects.size(), 0); + } + std::set refSet; // We use a vector here to keep the order (roughly) the same as ExternalGeometry std::vector > > newGeos; diff --git a/src/Mod/Sketcher/Gui/CommandSketcherOverlay.cpp b/src/Mod/Sketcher/Gui/CommandSketcherOverlay.cpp index a9c46f6582..98dc29f1ef 100644 --- a/src/Mod/Sketcher/Gui/CommandSketcherOverlay.cpp +++ b/src/Mod/Sketcher/Gui/CommandSketcherOverlay.cpp @@ -293,50 +293,50 @@ void CmdSketcherCompBSplineShowHideGeometryInformation::languageChange() QAction* c1 = a[0]; c1->setText(QApplication::translate("CmdSketcherCompBSplineShowHideGeometryInformation", - "Show/hide B-spline degree")); - c1->setToolTip(QApplication::translate( - "Sketcher_BSplineDegree", - "Switches between showing and hiding the degree for all B-splines")); - c1->setStatusTip(QApplication::translate( - "Sketcher_BSplineDegree", - "Switches between showing and hiding the degree for all B-splines")); + "Toggle B-Spline Degree")); + c1->setToolTip( + QApplication::translate("Sketcher_BSplineDegree", + "Toggles the visibility of the degree for all B-splines")); + c1->setStatusTip( + QApplication::translate("Sketcher_BSplineDegree", + "Toggles the visibility of the degree for all B-splines")); QAction* c2 = a[1]; c2->setText(QApplication::translate("CmdSketcherCompBSplineShowHideGeometryInformation", - "Show/hide B-spline control polygon")); + "Toggle B-Spline Control Polygon")); c2->setToolTip(QApplication::translate( "Sketcher_BSplinePolygon", - "Switches between showing and hiding the control polygons for all B-splines")); + "Toggles the visibility of the control polygons for all B-splines")); c2->setStatusTip(QApplication::translate( "Sketcher_BSplinePolygon", - "Switches between showing and hiding the control polygons for all B-splines")); + "Toggles the visibility of the control polygons for all B-splines")); QAction* c3 = a[2]; c3->setText(QApplication::translate("CmdSketcherCompBSplineShowHideGeometryInformation", - "Show/hide B-spline curvature comb")); - c3->setToolTip(QApplication::translate( - "Sketcher_BSplineComb", - "Switches between showing and hiding the curvature comb for all B-splines")); - c3->setStatusTip(QApplication::translate( - "Sketcher_BSplineComb", - "Switches between showing and hiding the curvature comb for all B-splines")); + "Toggle B-Spline Curvature Comb")); + c3->setToolTip( + QApplication::translate("Sketcher_BSplineComb", + "Toggles the visibility of the curvature comb for all B-splines")); + c3->setStatusTip( + QApplication::translate("Sketcher_BSplineComb", + "Toggles the visibility of the curvature comb for all B-splines")); QAction* c4 = a[3]; c4->setText(QApplication::translate("CmdSketcherCompBSplineShowHideGeometryInformation", - "Show/hide B-spline knot multiplicity")); + "Toggle B-Spline Knot Multiplicity")); c4->setToolTip(QApplication::translate( "Sketcher_BSplineKnotMultiplicity", - "Switches between showing and hiding the knot multiplicity for all B-splines")); + "Toggles the visibility of the knot multiplicity for all B-splines")); c4->setStatusTip(QApplication::translate( "Sketcher_BSplineKnotMultiplicity", - "Switches between showing and hiding the knot multiplicity for all B-splines")); + "Toggles the visibility of the knot multiplicity for all B-splines")); QAction* c5 = a[4]; c5->setText(QApplication::translate("CmdSketcherCompBSplineShowHideGeometryInformation", - "Show/hide B-spline control point weight")); + "Toggle B-Spline Control Point Weight")); c5->setToolTip(QApplication::translate( "Sketcher_BSplinePoleWeight", - "Switches between showing and hiding the control point weight for all B-splines")); + "Toggles the visibility of the control point weight for all B-splines")); c5->setStatusTip(QApplication::translate( "Sketcher_BSplinePoleWeight", - "Switches between showing and hiding the control point weight for all B-splines")); + "Toggles the visibility of the control point weight for all B-splines")); } void CmdSketcherCompBSplineShowHideGeometryInformation::updateAction(int /*mode*/) diff --git a/src/Mod/Sketcher/Gui/CommandSketcherTools.cpp b/src/Mod/Sketcher/Gui/CommandSketcherTools.cpp index 554c87eb35..72535b7191 100644 --- a/src/Mod/Sketcher/Gui/CommandSketcherTools.cpp +++ b/src/Mod/Sketcher/Gui/CommandSketcherTools.cpp @@ -2406,7 +2406,7 @@ CmdSketcherRotate::CmdSketcherRotate() { sAppModule = "Sketcher"; sGroup = "Sketcher"; - sMenuText = QT_TR_NOOP("Rotate/Polar Transform"); + sMenuText = QT_TR_NOOP("Rotate / Polar Transform"); sToolTipText = QT_TR_NOOP("Rotates the selected geometry by creating 'n' copies, enabling circular pattern creation"); sWhatsThis = "Sketcher_Rotate"; sStatusTip = sToolTipText; diff --git a/src/Mod/Sketcher/Gui/EditDatumDialog.cpp b/src/Mod/Sketcher/Gui/EditDatumDialog.cpp index b2278d8530..27e3258ce9 100644 --- a/src/Mod/Sketcher/Gui/EditDatumDialog.cpp +++ b/src/Mod/Sketcher/Gui/EditDatumDialog.cpp @@ -412,12 +412,22 @@ void EditDatumDialog::performAutoScale(double newDatum) && sketch->getExternalGeometryCount() <= 2 && sketch->hasSingleScaleDefiningConstraint()) { try { double oldDatum = sketch->getDatum(ConstrNbr); - double scale_factor = newDatum / oldDatum; + double scaleFactor = newDatum / oldDatum; float initLabelDistance = sketch->Constraints[ConstrNbr]->LabelDistance; float initLabelPosition = sketch->Constraints[ConstrNbr]->LabelPosition; - centerScale(sketch, scale_factor); - sketch->setLabelDistance(ConstrNbr, initLabelDistance * scale_factor); - sketch->setLabelPosition(ConstrNbr, initLabelPosition * scale_factor); + centerScale(sketch, scaleFactor); + sketch->setLabelDistance(ConstrNbr, initLabelDistance * scaleFactor); + + // Label position or radii and diameters represent an angle, so + // they should not be scaled + Sketcher::ConstraintType type = sketch->Constraints[ConstrNbr]->Type; + if (type == Sketcher::ConstraintType::Radius + || type == Sketcher::ConstraintType::Diameter) { + sketch->setLabelPosition(ConstrNbr, initLabelPosition); + } + else { + sketch->setLabelPosition(ConstrNbr, initLabelPosition * scaleFactor); + } } catch (const Base::Exception& e) { Base::Console().error("Exception performing autoscale: %s\n", e.what()); diff --git a/src/Mod/Sketcher/Gui/SketcherSettings.ui b/src/Mod/Sketcher/Gui/SketcherSettings.ui index ca3ef8bc83..37d3e1c9db 100644 --- a/src/Mod/Sketcher/Gui/SketcherSettings.ui +++ b/src/Mod/Sketcher/Gui/SketcherSettings.ui @@ -293,21 +293,12 @@ This setting is only for the toolbar. Whichever you choose, all tools are always - - - Always - - - - - Never - - - - - Only if there is no visual scale indicator - - + + Select the mode of automatic geometry scaling upon first dimension: +'Always': Automatic scaling upon first dimension is always performed. +'Never': Automatic scaling upon first dimension is never performed. +'When no scale feature is visible': Automatic scaling upon first dimension is only performed if there are no visible objects in the 3D view. + diff --git a/src/Mod/Sketcher/Gui/Workbench.cpp b/src/Mod/Sketcher/Gui/Workbench.cpp index 27f4760869..6ee89c8fb1 100644 --- a/src/Mod/Sketcher/Gui/Workbench.cpp +++ b/src/Mod/Sketcher/Gui/Workbench.cpp @@ -37,7 +37,7 @@ using namespace SketcherGui; qApp->translate("Workbench","P&rofiles"); qApp->translate("Workbench","S&ketch"); qApp->translate("Workbench", "Sketcher"); - qApp->translate("Workbench", "Sketcher Edit Mode"); + qApp->translate("Workbench", "Edit Mode"); qApp->translate("Workbench", "Geometries"); qApp->translate("Workbench", "Constraints"); @@ -113,7 +113,7 @@ Gui::ToolBarItem* Workbench::setupToolBars() const Gui::ToolBarItem* sketcherEditMode = new Gui::ToolBarItem(root, Gui::ToolBarItem::DefaultVisibility::Unavailable); - sketcherEditMode->setCommand("Sketcher Edit Mode"); + sketcherEditMode->setCommand("Edit Mode"); addSketcherWorkbenchSketchEditModeActions(*sketcherEditMode); Gui::ToolBarItem* geom = diff --git a/tests/src/Gui/CMakeLists.txt b/tests/src/Gui/CMakeLists.txt index 774cf35f31..2c1e3f8ded 100644 --- a/tests/src/Gui/CMakeLists.txt +++ b/tests/src/Gui/CMakeLists.txt @@ -2,6 +2,9 @@ target_sources(Tests_run PRIVATE Assistant.cpp Camera.cpp + StyleParameters/StyleParametersApplicationTest.cpp + StyleParameters/ParserTest.cpp + StyleParameters/ParameterManagerTest.cpp ) # Qt tests diff --git a/tests/src/Gui/StyleParameters/ParameterManagerTest.cpp b/tests/src/Gui/StyleParameters/ParameterManagerTest.cpp new file mode 100644 index 0000000000..09c40cf13b --- /dev/null +++ b/tests/src/Gui/StyleParameters/ParameterManagerTest.cpp @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kacper Donat * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD 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. * + * * + * FreeCAD 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 FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#include + +#include +#include + +using namespace Gui::StyleParameters; + +class ParameterManagerTest: public ::testing::Test +{ +protected: + void SetUp() override + { + // Create test sources + auto source1 = std::make_unique( + std::list { + {"BaseSize", "8px"}, + {"PrimaryColor", "#ff0000"}, + {"SecondaryColor", "#00ff00"}, + }, + ParameterSource::Metadata {"Source 1"}); + + auto source2 = std::make_unique( + std::list { + {"BaseSize", "16px"}, // Override from source1 + {"Margin", "@BaseSize * 2"}, + {"Padding", "@BaseSize / 2"}, + }, + ParameterSource::Metadata {"Source 2"}); + + manager.addSource(source1.get()); + manager.addSource(source2.get()); + sources.push_back(std::move(source1)); + sources.push_back(std::move(source2)); + } + + Gui::StyleParameters::ParameterManager manager; + std::vector> sources; +}; + +// Test basic parameter resolution +TEST_F(ParameterManagerTest, BasicParameterResolution) +{ + { + auto result = manager.resolve("BaseSize"); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 16.0); // Should get value from source2 (later source) + EXPECT_EQ(length.unit, "px"); + } + + { + auto result = manager.resolve("PrimaryColor"); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + EXPECT_EQ(color.red(), 255); + EXPECT_EQ(color.green(), 0); + EXPECT_EQ(color.blue(), 0); + } + + { + auto result = manager.resolve("SecondaryColor"); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + EXPECT_EQ(color.red(), 0); + EXPECT_EQ(color.green(), 255); + EXPECT_EQ(color.blue(), 0); + } +} + +// Test parameter references +TEST_F(ParameterManagerTest, ParameterReferences) +{ + { + auto result = manager.resolve("Margin"); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 32.0); // @BaseSize * 2 = 16 * 2 = 32 + EXPECT_EQ(length.unit, "px"); + } + + { + auto result = manager.resolve("Padding"); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 8.0); // @BaseSize / 2 = 16 / 2 = 8 + EXPECT_EQ(length.unit, "px"); + } +} + +// Test caching +TEST_F(ParameterManagerTest, Caching) +{ + // First resolution should cache the result + auto result1 = manager.resolve("BaseSize"); + EXPECT_TRUE(std::holds_alternative(result1)); + + // Second resolution should use cached value + auto result2 = manager.resolve("BaseSize"); + EXPECT_TRUE(std::holds_alternative(result2)); + + // Results should be identical + auto length1 = std::get(result1); + auto length2 = std::get(result2); + EXPECT_DOUBLE_EQ(length1.value, length2.value); + EXPECT_EQ(length1.unit, length2.unit); +} + +// Test cache invalidation +TEST_F(ParameterManagerTest, CacheInvalidation) +{ + // Initial resolution + auto result1 = manager.resolve("BaseSize"); + EXPECT_TRUE(std::holds_alternative(result1)); + auto length1 = std::get(result1); + EXPECT_DOUBLE_EQ(length1.value, 16.0); + + // Reload should clear cache + manager.reload(); + + // Resolution after reload should work the same + auto result2 = manager.resolve("BaseSize"); + EXPECT_TRUE(std::holds_alternative(result2)); + auto length2 = std::get(result2); + EXPECT_DOUBLE_EQ(length2.value, 16.0); + EXPECT_EQ(length1.unit, length2.unit); +} + +// Test source priority +TEST_F(ParameterManagerTest, SourcePriority) +{ + // Create a third source with higher priority + auto source3 = std::make_unique( + std::list { + {"BaseSize", "24px"}, // Should override both previous sources + }, + ParameterSource::Metadata {"Source 3"}); + + manager.addSource(source3.get()); + sources.push_back(std::move(source3)); + + // Should get value from the latest source (highest priority) + auto result = manager.resolve("BaseSize"); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 24.0); + EXPECT_EQ(length.unit, "px"); +} + +// Test parameter listing +TEST_F(ParameterManagerTest, ParameterListing) +{ + auto params = manager.parameters(); + + // Should contain all parameters from all sources + std::set paramNames; + for (const auto& param : params) { + paramNames.insert(param.name); + } + + EXPECT_TRUE(paramNames.contains("BaseSize")); + EXPECT_TRUE(paramNames.contains("PrimaryColor")); + EXPECT_TRUE(paramNames.contains("SecondaryColor")); + EXPECT_TRUE(paramNames.contains("Margin")); + EXPECT_TRUE(paramNames.contains("Padding")); + + // Should not contain duplicates (BaseSize should appear only once) + EXPECT_EQ(paramNames.count("BaseSize"), 1); +} + +// Test expression retrieval +TEST_F(ParameterManagerTest, ExpressionRetrieval) +{ + { + auto expr = manager.expression("BaseSize"); + EXPECT_TRUE(expr.has_value()); + EXPECT_EQ(*expr, "16px"); + } + + { + auto expr = manager.expression("Margin"); + EXPECT_TRUE(expr.has_value()); + EXPECT_EQ(*expr, "@BaseSize * 2"); + } + + { + auto expr = manager.expression("NonExistent"); + EXPECT_FALSE(expr.has_value()); + } +} + +// Test parameter retrieval +TEST_F(ParameterManagerTest, ParameterRetrieval) +{ + { + auto param = manager.parameter("BaseSize"); + EXPECT_TRUE(param.has_value()); + EXPECT_EQ(param->name, "BaseSize"); + EXPECT_EQ(param->value, "16px"); + } + + { + auto param = manager.parameter("NonExistent"); + EXPECT_FALSE(param.has_value()); + } +} + +// Test source management +TEST_F(ParameterManagerTest, SourceManagement) +{ + auto sources = manager.sources(); + EXPECT_EQ(sources.size(), 2); // We added 2 sources in SetUp + + // Test that we can access the sources + for (auto source : sources) { + EXPECT_NE(source, nullptr); + auto params = source->all(); + EXPECT_FALSE(params.empty()); + } +} + +// Test circular reference detection +TEST_F(ParameterManagerTest, CircularReferenceDetection) +{ + // Create a source with circular reference + auto circularSource = std::make_unique( + std::list { + {"A", "@B"}, + {"B", "@A"}, + }, + ParameterSource::Metadata {"Circular Source"}); + + manager.addSource(circularSource.get()); + sources.push_back(std::move(circularSource)); + + // Should handle circular reference gracefully + auto result = manager.resolve("A"); + // Should return the expression string as fallback + EXPECT_TRUE(std::holds_alternative(result)); +} + +// Test complex expressions +TEST_F(ParameterManagerTest, ComplexExpressions) +{ + // Create a source with complex expressions + auto complexSource = std::make_unique( + std::list { + {"ComplexMargin", "(@BaseSize + 4px) * 2"}, + {"ComplexPadding", "(@BaseSize - 2px) / 2"}, + {"ColorWithFunction", "lighten(@PrimaryColor, 20)"}, + }, + ParameterSource::Metadata {"Complex Source"}); + + manager.addSource(complexSource.get()); + sources.push_back(std::move(complexSource)); + + { + auto result = manager.resolve("ComplexMargin"); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 40.0); // (16 + 4) * 2 = 20 * 2 = 40 + EXPECT_EQ(length.unit, "px"); + } + + { + auto result = manager.resolve("ComplexPadding"); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 7.0); // (16 - 2) / 2 = 14 / 2 = 7 + EXPECT_EQ(length.unit, "px"); + } + + { + auto result = manager.resolve("ColorWithFunction"); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + // Should be lighter than the original red + EXPECT_GT(color.lightness(), QColor("#ff0000").lightness()); + } +} + +// Test error handling +TEST_F(ParameterManagerTest, ErrorHandling) +{ + // Test non-existent parameter + auto result = manager.resolve("NonExistent"); + EXPECT_TRUE(std::holds_alternative(result)); + EXPECT_EQ(std::get(result), ""); + + // Test invalid expression + auto invalidSource = std::make_unique( + std::list { + {"Invalid", "invalid expression that will fail"}, + }, + ParameterSource::Metadata {"Invalid Source"}); + + manager.addSource(invalidSource.get()); + sources.push_back(std::move(invalidSource)); + + // Should handle invalid expression gracefully + auto invalidResult = manager.resolve("Invalid"); + // Should return the expression string as fallback + EXPECT_TRUE(std::holds_alternative(invalidResult)); +} diff --git a/tests/src/Gui/StyleParameters/ParserTest.cpp b/tests/src/Gui/StyleParameters/ParserTest.cpp new file mode 100644 index 0000000000..a7e1b50734 --- /dev/null +++ b/tests/src/Gui/StyleParameters/ParserTest.cpp @@ -0,0 +1,617 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kacper Donat * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD 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. * + * * + * FreeCAD 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 FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#include + +#include + +#include +#include + +using namespace Gui::StyleParameters; + +class ParserTest: public ::testing::Test +{ +protected: + void SetUp() override + { + // Create a simple parameter manager for testing + auto source = std::make_unique( + std::list { + {"TestParam", "10px"}, + {"TestColor", "#ff0000"}, + {"TestNumber", "5"}, + }, + ParameterSource::Metadata {"Test Source"}); + + manager.addSource(source.get()); + sources.push_back(std::move(source)); + } + + Gui::StyleParameters::ParameterManager manager; + std::vector> sources; +}; + +// Test number parsing +TEST_F(ParserTest, ParseNumbers) +{ + { + Parser parser("42"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 42.0); + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("10.5px"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 10.5); + EXPECT_EQ(length.unit, "px"); + } + + { + Parser parser("2.5em"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 2.5); + EXPECT_EQ(length.unit, "em"); + } + + { + Parser parser("100%"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 100.0); + EXPECT_EQ(length.unit, "%"); + } +} + +// Test color parsing +TEST_F(ParserTest, ParseColors) +{ + { + Parser parser("#ff0000"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + EXPECT_EQ(color.red(), 255); + EXPECT_EQ(color.green(), 0); + EXPECT_EQ(color.blue(), 0); + } + + { + Parser parser("#00ff00"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + EXPECT_EQ(color.red(), 0); + EXPECT_EQ(color.green(), 255); + EXPECT_EQ(color.blue(), 0); + } + + { + Parser parser("#0000ff"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + EXPECT_EQ(color.red(), 0); + EXPECT_EQ(color.green(), 0); + EXPECT_EQ(color.blue(), 255); + } + + { + Parser parser("rgb(255, 0, 0)"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + EXPECT_EQ(color.red(), 255); + EXPECT_EQ(color.green(), 0); + EXPECT_EQ(color.blue(), 0); + } + + { + Parser parser("rgba(255, 0, 0, 128)"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + EXPECT_EQ(color.red(), 255); + EXPECT_EQ(color.green(), 0); + EXPECT_EQ(color.blue(), 0); + EXPECT_EQ(color.alpha(), 128); + } +} + +// Test parameter reference parsing +TEST_F(ParserTest, ParseParameterReferences) +{ + { + Parser parser("@TestParam"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 10.0); + EXPECT_EQ(length.unit, "px"); + } + + { + Parser parser("@TestColor"); + auto expr = parser.parse(); + auto result = expr->evaluate({.manager = &manager, .context = {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + EXPECT_EQ(color.red(), 255); + EXPECT_EQ(color.green(), 0); + EXPECT_EQ(color.blue(), 0); + } + + { + Parser parser("@TestNumber"); + auto expr = parser.parse(); + auto result = expr->evaluate({.manager = &manager, .context = {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 5.0); + EXPECT_EQ(length.unit, ""); + } +} + +// Test arithmetic operations +TEST_F(ParserTest, ParseArithmeticOperations) +{ + { + Parser parser("10 + 5"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 15.0); + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("10px + 5px"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 15.0); + EXPECT_EQ(length.unit, "px"); + } + + { + Parser parser("10 - 5"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 5.0); + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("10px - 5px"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 5.0); + EXPECT_EQ(length.unit, "px"); + } + + { + Parser parser("10 * 5"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 50.0); + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("10px * 2"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 20.0); + EXPECT_EQ(length.unit, "px"); + } + + { + Parser parser("10 / 2"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 5.0); + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("10px / 2"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 5.0); + EXPECT_EQ(length.unit, "px"); + } +} + +// Test complex expressions +TEST_F(ParserTest, ParseComplexExpressions) +{ + { + Parser parser("(10 + 5) * 2"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 30.0); + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("(10px + 5px) * 2"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 30.0); + EXPECT_EQ(length.unit, "px"); + } + + { + Parser parser("@TestParam + 5px"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 15.0); + EXPECT_EQ(length.unit, "px"); + } + + { + Parser parser("@TestParam * @TestNumber"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 50.0); + EXPECT_EQ(length.unit, "px"); + } +} + +// Test unary operations +TEST_F(ParserTest, ParseUnaryOperations) +{ + { + Parser parser("+10"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 10.0); + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("-10"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, -10.0); + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("-10px"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, -10.0); + EXPECT_EQ(length.unit, "px"); + } +} + +// Test function calls +TEST_F(ParserTest, ParseFunctionCalls) +{ + { + Parser parser("lighten(#ff0000, 20)"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + // The result should be lighter than the original red + EXPECT_GT(color.lightness(), QColor("#ff0000").lightness()); + } + + { + Parser parser("darken(#ff0000, 20)"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + // The result should be darker than the original red + EXPECT_LT(color.lightness(), QColor("#ff0000").lightness()); + } + + { + Parser parser("lighten(@TestColor, 20)"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + // The result should be lighter than the original red + EXPECT_GT(color.lightness(), QColor("#ff0000").lightness()); + } +} + +// Test error cases +TEST_F(ParserTest, ParseErrors) +{ + // Invalid color format + EXPECT_THROW( + { + Parser parser("#invalid"); + parser.parse(); + }, + Base::ParserError); + + // Invalid RGB format + EXPECT_THROW( + { + Parser parser("rgb(invalid)"); + parser.parse(); + }, + Base::ParserError); + + // Missing closing parenthesis + EXPECT_THROW( + { + Parser parser("(10 + 5"); + parser.parse(); + }, + Base::ParserError); + + // Invalid function + EXPECT_THROW( + { + Parser parser("invalid()"); + auto expr = parser.parse(); + expr->evaluate({&manager, {}}); + }, + Base::ExpressionError); + + // Division by zero + EXPECT_THROW( + { + Parser parser("10 / 0"); + auto expr = parser.parse(); + expr->evaluate({&manager, {}}); + }, + Base::RuntimeError); + + // Unit mismatch + EXPECT_THROW( + { + Parser parser("10px + 5em"); + auto expr = parser.parse(); + expr->evaluate({&manager, {}}); + }, + Base::RuntimeError); + + // Unary operation on color + EXPECT_THROW( + { + Parser parser("-@TestColor"); + auto expr = parser.parse(); + expr->evaluate({&manager, {}}); + }, + Base::ExpressionError); + + // Function with wrong number of arguments + EXPECT_THROW( + { + Parser parser("lighten(#ff0000)"); + auto expr = parser.parse(); + expr->evaluate({&manager, {}}); + }, + Base::ExpressionError); + + // Function with wrong argument type + EXPECT_THROW( + { + Parser parser("lighten(10px, 20)"); + auto expr = parser.parse(); + expr->evaluate({&manager, {}}); + }, + Base::ExpressionError); +} + +// Test whitespace handling +TEST_F(ParserTest, ParseWhitespace) +{ + { + Parser parser(" 10 + 5 "); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 15.0); + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("10px+5px"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 15.0); + EXPECT_EQ(length.unit, "px"); + } + + { + Parser parser("rgb(255,0,0)"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + EXPECT_EQ(color.red(), 255); + EXPECT_EQ(color.green(), 0); + EXPECT_EQ(color.blue(), 0); + } +} + +// Test edge cases +TEST_F(ParserTest, ParseEdgeCases) +{ + // Empty input + EXPECT_THROW( + { + Parser parser(""); + parser.parse(); + }, + Base::ParserError); + + // Just whitespace + EXPECT_THROW( + { + Parser parser(" "); + parser.parse(); + }, + Base::ParserError); + + // Single number + { + Parser parser("42"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 42.0); + EXPECT_EQ(length.unit, ""); + } + + // Single color + { + Parser parser("#ff0000"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto color = std::get(result); + EXPECT_EQ(color.red(), 255); + EXPECT_EQ(color.green(), 0); + EXPECT_EQ(color.blue(), 0); + } + + // Single parameter reference + { + Parser parser("@TestParam"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 10.0); + EXPECT_EQ(length.unit, "px"); + } +} + +// Test operator precedence +TEST_F(ParserTest, ParseOperatorPrecedence) +{ + { + Parser parser("2 + 3 * 4"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 14.0); // 2 + (3 * 4) = 2 + 12 = 14 + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("10 - 3 * 2"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 4.0); // 10 - (3 * 2) = 10 - 6 = 4 + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("20 / 4 + 3"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 8.0); // (20 / 4) + 3 = 5 + 3 = 8 + EXPECT_EQ(length.unit, ""); + } +} + +// Test nested parentheses +TEST_F(ParserTest, ParseNestedParentheses) +{ + { + Parser parser("((2 + 3) * 4)"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 20.0); // (5) * 4 = 20 + EXPECT_EQ(length.unit, ""); + } + + { + Parser parser("(10 - (3 + 2)) * 2"); + auto expr = parser.parse(); + auto result = expr->evaluate({&manager, {}}); + EXPECT_TRUE(std::holds_alternative(result)); + auto length = std::get(result); + EXPECT_DOUBLE_EQ(length.value, 10.0); // (10 - 5) * 2 = 5 * 2 = 10 + EXPECT_EQ(length.unit, ""); + } +} diff --git a/tests/src/Gui/StyleParameters/StyleParametersApplicationTest.cpp b/tests/src/Gui/StyleParameters/StyleParametersApplicationTest.cpp new file mode 100644 index 0000000000..cacdfeea91 --- /dev/null +++ b/tests/src/Gui/StyleParameters/StyleParametersApplicationTest.cpp @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kacper Donat * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD 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. * + * * + * FreeCAD 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 FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#include + +#include "src/App/InitApplication.h" + +#include +#include + +using namespace Gui; + +class StyleParametersApplicationTest: public ::testing::Test +{ +protected: + static Application* app; + + static void SetUpTestSuite() + { + tests::initApplication(); + app = new Application(true); + } + + void SetUp() override + { + auto styleParamManager = app->styleParameterManager(); + + styleParamManager->addSource(new StyleParameters::InMemoryParameterSource( + { + {.name = "ColorPrimary", .value = "#ff0000"}, + {.name = "FontSize", .value = "12px"}, + {.name = "BoxWidth", .value = "100px"}, + }, + {.name = "Fixture Source"})); + } +}; + +Application* StyleParametersApplicationTest::app = {}; + +// Test for replacing variables in QSS string +TEST_F(StyleParametersApplicationTest, ReplaceVariablesInQss) +{ + QString qss = "QWidget { color: @ColorPrimary; font-size: @FontSize; width: @BoxWidth; }"; + QString result = app->replaceVariablesInQss(qss); + + EXPECT_EQ(result.toStdString(), "QWidget { color: #ff0000; font-size: 12px; width: 100px; }"); +} + +// Test if unknown variables remain unchanged +TEST_F(StyleParametersApplicationTest, ReplaceVariablesInQssWithUnknownVariable) +{ + QString qss = "QWidget { color: @UnknownColor; margin: 10px; }"; + QString result = app->replaceVariablesInQss(qss); + + EXPECT_EQ(result.toStdString(), "QWidget { color: ; margin: 10px; }"); +} + +// Test with an empty QSS string +TEST_F(StyleParametersApplicationTest, ReplaceVariablesInQssWithEmptyString) +{ + QString qss = ""; + QString result = app->replaceVariablesInQss(qss); + + EXPECT_EQ(result.toStdString(), ""); +} + +// Test replacing multiple occurrences of the same variable +TEST_F(StyleParametersApplicationTest, ReplaceVariablesInQssWithMultipleOccurrences) +{ + QString qss = "QWidget { color: @ColorPrimary; background: @ColorPrimary; }"; + QString result = app->replaceVariablesInQss(qss); + + EXPECT_EQ(result.toStdString(), "QWidget { color: #ff0000; background: #ff0000; }"); +} diff --git a/tests/src/Mod/Part/App/PartFeatures.cpp b/tests/src/Mod/Part/App/PartFeatures.cpp index 550a478356..d5f07f8e6c 100644 --- a/tests/src/Mod/Part/App/PartFeatures.cpp +++ b/tests/src/Mod/Part/App/PartFeatures.cpp @@ -106,6 +106,7 @@ TEST_F(PartFeaturesTest, testSweep) auto _sweep = _doc->addObject(); _sweep->Sections.setValues({_plane1}); _sweep->Spine.setValue(_edge1); + _sweep->Solid.setValue((false)); // Act _sweep->execute(); TopoShape ts = _sweep->Shape.getShape();