Merge branch 'main' into CAM-Inspect-Fix

This commit is contained in:
Ian Abreu
2025-07-09 19:40:30 -04:00
committed by GitHub
89 changed files with 4367 additions and 902 deletions
+1 -1
View File
@@ -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
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+6 -5
View File
@@ -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'
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
-3
View File
@@ -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",
-1
View File
@@ -1 +0,0 @@
mamba run --live-stream -n freecad cmake %*
-3
View File
@@ -1,3 +0,0 @@
#!/bin/sh
mamba run --live-stream -n freecad cmake $@
-9
View File
@@ -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
-9
View File
@@ -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
-101
View File
@@ -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
-101
View File
@@ -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
-12
View File
@@ -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
-10
View File
@@ -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
-12
View File
@@ -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
+2 -2
View File
@@ -100,7 +100,7 @@
},
{
"name": "Python debugger",
"type": "python",
"type": "debugpy",
"request": "attach",
"preLaunchTask": "WaitForDebugpy",
"redirectOutput": true,
@@ -131,4 +131,4 @@
}
}
]
}
}
+27 -21
View File
@@ -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"
+27
View File
@@ -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<class... Ts>
struct Overloads: Ts...
{
using Ts::operator()...;
};
} // namespace Base
#endif // SRC_BASE_TOOLS_H_
+53 -28
View File
@@ -137,9 +137,11 @@
#include "WorkbenchManipulator.h"
#include "WidgetFactory.h"
#include "3Dconnexion/navlib/NavlibInterface.h"
#include "Inventor/SoFCPlacementIndicatorKit.h"
#include "QtWidgets.h"
#include <Inventor/SoFCPlacementIndicatorKit.h>
#include <OverlayManager.h>
#include <Base/ServiceProvider.h>
#ifdef BUILD_TRACY_FRAME_PROFILER
#include <tracy/Tracy.hpp>
@@ -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<Gui::BaseView*> passive;
bool isClosing {false};
@@ -372,6 +379,31 @@ struct PyMethodDef FreeCADGui_methods[] = {
} // namespace Gui
void Application::initStyleParameterManager()
{
Base::registerServiceImplementation<StyleParameters::ParameterSource>(
new StyleParameters::BuiltInParameterSource({.name = QT_TR_NOOP("Built-in Parameters")}));
Base::registerServiceImplementation<StyleParameters::ParameterSource>(
new StyleParameters::UserParameterSource(
App::GetApplication().GetParameterGroupByPath(
"User parameter:BaseApp/Preferences/Themes/Tokens"),
{.name = QT_TR_NOOP("Theme Parameters"),
.options = StyleParameters::ParameterSourceOption::UserEditable}));
Base::registerServiceImplementation<StyleParameters::ParameterSource>(
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<StyleParameters::ParameterSource>()) {
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()
+8 -1
View File
@@ -30,6 +30,8 @@
#include <App/Application.h>
#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 */
//@{
+8
View File
@@ -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
+39 -93
View File
@@ -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());
}
+764
View File
@@ -0,0 +1,764 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <[email protected]> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#include "PreCompiled.h"
#include "Tools.h"
#include "Application.h"
#include "OverlayManager.h"
#include "DlgThemeEditor.h"
#include "ui_DlgThemeEditor.h"
#include "BitmapFactory.h"
#include <Base/ServiceProvider.h>
#include <Base/Tools.h>
#ifndef _PreComp_
# include <ranges>
# include <QImageReader>
# include <QPainter>
# include <QStyledItemDelegate>
# include <QTimer>
#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<std::string> 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<Qt::ItemFlag> flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
};
class StyleParametersModel::Node
{
public:
explicit Node(std::unique_ptr<Item> data, Node* parent = nullptr)
: _parent(parent)
, _data(std::move(data))
{}
void appendChild(std::unique_ptr<Node> child)
{
child->_parent = this;
_children.push_back(std::move(child));
}
void removeChild(const int row)
{
if (row >= 0 && row < static_cast<int>(_children.size())) {
_children.erase(_children.begin() + row);
}
}
Node* child(const int row) const
{
if (row < 0 || row >= static_cast<int>(_children.size())) {
if (!_empty) {
_empty = std::make_unique<Node>(nullptr, const_cast<Node*>(this));
}
return _empty.get();
}
return _children[row].get();
}
int childCount() const
{
return static_cast<int>(_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<int>(i);
}
}
return -1;
}
Item* data() const
{
return _data.get();
}
template<class T>
T* data() const
{
return dynamic_cast<T*>(_data.get());
}
Node* parent() const
{
return _parent;
}
private:
Node* _parent;
std::vector<std::unique_ptr<Node>> _children {};
mutable std::unique_ptr<Node> _empty {};
std::unique_ptr<Item> _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<const StyleParametersModel*>(index.model());
if (!model) {
return nullptr;
}
if (model->item<StyleParametersModel::ParameterItem>(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<QLineEdit*>(editor)) {
lineEdit->setText(index.data(Qt::DisplayRole).toString());
}
}
void setModelData(QWidget* editor,
QAbstractItemModel* model,
const QModelIndex& index) const override
{
if (auto* lineEdit = qobject_cast<QLineEdit*>(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<const StyleParametersModel*>(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<StyleParametersModel::GroupItem>(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<StyleParameters::ParameterSource*>& 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<StyleParameters::Parameter> StyleParametersModel::all() const
{
std::map<std::string, StyleParameters::Parameter> result;
QtTools::walkTreeModel(this, [this, &result](const QModelIndex& index) {
if (auto parameterItem = item<ParameterItem>(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<StyleParameters::Parameter>(values.begin(), values.end());
}
std::optional<StyleParameters::Parameter> StyleParametersModel::get(const std::string& name) const
{
std::optional<StyleParameters::Parameter> result = std::nullopt;
QtTools::walkTreeModel(this, [this, &name, &result](const QModelIndex& index) {
if (auto parameterItem = item<ParameterItem>(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<ParameterItem>(index)) {
auto groupItem = item<GroupItem>(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<Node>(std::make_unique<GroupItem>(tr("Root"), nullptr));
for (auto* source : sources) {
auto groupNode = std::make_unique<Node>(
std::make_unique<GroupItem>(tr(source->metadata.name.c_str()), source));
for (const auto& parameter : source->all()) {
auto item = std::make_unique<Node>(
std::make_unique<ParameterItem>(QString::fromStdString(parameter.name), parameter));
if (source->metadata.options.testFlag(ReadOnly)) {
item->data<ParameterItem>()->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<GroupItem>(index)) {
for (const auto& parameter : groupItem->deleted) {
groupItem->source->remove(parameter);
}
groupItem->deleted.clear();
}
if (const auto& parameterItem = item<ParameterItem>(index)) {
const auto& groupItem = item<GroupItem>(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<GroupItem>(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<ParameterItem>(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<QColor>(value)) {
return colorPreview(std::get<QColor>(value));
}
}
}
if (auto groupItem = item<GroupItem>(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<ParameterItem>(index)) {
auto groupItem = item<GroupItem>(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<Node>(
std::make_unique<ParameterItem>(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<ParameterItem>(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<Node*>(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<Node*>(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<StyleParametersModel>(
Base::provideServiceImplementations<StyleParameters::ParameterSource>(),
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<ColumnDefinition> 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<const QModelIndex&>(&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"
+152
View File
@@ -0,0 +1,152 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <[email protected]> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#ifndef DLGTHEMEEDITOR_H
#define DLGTHEMEEDITOR_H
#include "StyleParameters/ParameterManager.h"
#include <QDialog>
#include <QTreeView>
#include <optional>
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<ParameterSource*>& sources,
QObject* parent = nullptr);
~StyleParametersModel() override;
std::list<StyleParameters::Parameter> all() const override;
std::optional<StyleParameters::Parameter> 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 <typename T>
T* item(const QModelIndex& index) const
{
return dynamic_cast<T*>(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<ParameterSource*> sources;
std::unique_ptr<StyleParameters::ParameterManager> manager;
std::unique_ptr<Node> 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::DlgThemeEditor> ui;
std::unique_ptr<StyleParameters::ParameterManager> manager;
std::unique_ptr<StyleParametersModel> model;
};
} // Gui
#endif //DLGTHEMEEDITOR_H
+156
View File
@@ -0,0 +1,156 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Gui::DlgThemeEditor</class>
<widget class="QDialog" name="Gui::DlgThemeEditor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1169</width>
<height>562</height>
</rect>
</property>
<property name="windowTitle">
<string>Theme Editor</string>
</property>
<layout class="QGridLayout" name="gridLayout" columnstretch="3,1">
<item row="1" column="1">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Preview</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter</set>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="3" column="1">
<widget class="QCheckBox" name="checkBox">
<property name="text">
<string>CheckBox</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QScrollBar" name="horizontalScrollBar">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QSpinBox" name="spinBox"/>
</item>
<item row="8" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="1">
<widget class="QRadioButton" name="radioButton">
<property name="text">
<string>RadioButton</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QComboBox" name="comboBox">
<item>
<property name="text">
<string>Item 1</string>
</property>
</item>
<item>
<property name="text">
<string>Item 2</string>
</property>
</item>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="pushButton">
<property name="text">
<string>PushButton</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QTabWidget" name="tabWidget">
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Tab 1</string>
</attribute>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Tab 2</string>
</attribute>
</widget>
</widget>
</item>
<item row="1" column="1">
<widget class="Gui::ColorButton" name="colorButton"/>
</item>
</layout>
</widget>
</item>
<item row="2" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Apply|QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok|QDialogButtonBox::StandardButton::Reset</set>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="Gui::TokenTreeView" name="tokensTreeView">
<property name="mouseTracking">
<bool>true</bool>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::EditTrigger::DoubleClicked</set>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>Gui::ColorButton</class>
<extends>QPushButton</extends>
<header>Gui/Widgets.h</header>
</customwidget>
<customwidget>
<class>Gui::TokenTreeView</class>
<extends>QTreeView</extends>
<header location="global">Gui/Dialogs/DlgThemeEditor.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
+3 -1
View File
@@ -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;
+2
View File
@@ -69,8 +69,10 @@
#include <list>
#include <map>
#include <numbers>
#include <optional>
#include <queue>
#include <random>
#include <ranges>
#include <set>
#include <sstream>
#include <stack>
+7 -1
View File
@@ -41,9 +41,12 @@
#include "DockWindowManager.h"
#include "ToolBarManager.h"
#include <Application.h>
#include <App/Application.h>
#include <ctime> // For generating a timestamped filename
#include <ctime> // For generating a timestamped filename
#include <Base/ServiceProvider.h>
#include <Dialogs/DlgThemeEditor.h>
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;
@@ -627,6 +627,7 @@
<FCText Name="Std_Delete">Del</FCText>
<FCText Name="Std_DemoMode"></FCText>
<FCText Name="Std_DependencyGraph"></FCText>
<FCText Name="Std_DevHandbook"></FCText>
<FCText Name="Std_DlgCustomize"></FCText>
<FCText Name="Std_DlgMacroExecute"></FCText>
<FCText Name="Std_DlgMacroExecuteDirect">Ctrl+F6</FCText>
@@ -642,9 +643,7 @@
<FCText Name="Std_Expressions"></FCText>
<FCText Name="Std_FaceSelection"></FCText>
<FCText Name="Std_FreeCADDonation"></FCText>
<FCText Name="Std_FreeCADFAQ"></FCText>
<FCText Name="Std_FreeCADForum"></FCText>
<FCText Name="Std_FreeCADPowerUserHub"></FCText>
<FCText Name="Std_FreeCADUserHub"></FCText>
<FCText Name="Std_FreeCADWebsite"></FCText>
<FCText Name="Std_FreezeViews"></FCText>
@@ -690,7 +689,6 @@
<FCText Name="Std_ProjectInfo"></FCText>
<FCText Name="Std_ProjectUtil"></FCText>
<FCText Name="Std_PythonHelp"></FCText>
<FCText Name="Std_PythonWebsite"></FCText>
<FCText Name="Std_Quit">Ctrl+Q</FCText>
<FCText Name="Std_RandomColor"></FCText>
<FCText Name="Std_RecallWorkingView">End</FCText>
+24 -5
View File
@@ -31,6 +31,10 @@
#include "DlgSettingsUI.h"
#include "ui_DlgSettingsUI.h"
#include "Dialogs/DlgThemeEditor.h"
#include <Base/ServiceProvider.h>
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<QString, QString> 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<Gui::StyleParameters::ParameterManager>()) {
parameterManager->reload();
}
auto sheet = hGrp->GetASCII("StyleSheet");
bool tiledBG = hGrp->GetBool("TiledBackground", false);
Gui::Application::Instance->setStyleSheet(QString::fromUtf8(sheet.c_str()), tiledBG);
+1
View File
@@ -62,6 +62,7 @@ protected:
const char *def,
QStringList filter = QStringList());
void openThemeEditor();
private:
std::unique_ptr<Ui_DlgSettingsUI> ui;
};
+34 -27
View File
@@ -31,7 +31,7 @@
</widget>
</item>
<item>
<layout class="QGridLayout" columnstretch="2,1,0">
<layout class="QGridLayout" columnstretch="2,1">
<item row="0" column="0">
<widget class="QLabel" name="label1">
<property name="text">
@@ -53,7 +53,7 @@
<property name="toolTip">
<string>This color might be used by your theme to let you customize it.</string>
</property>
<property name="color" stdset="0">
<property name="color">
<color>
<red>85</red>
<green>123</green>
@@ -119,7 +119,7 @@
<property name="toolTip">
<string>This color might be used by your theme to let you customize it.</string>
</property>
<property name="color" stdset="0">
<property name="color">
<color>
<red>85</red>
<green>123</green>
@@ -145,7 +145,7 @@
<property name="toolTip">
<string>This color might be used by your theme to let you customize it.</string>
</property>
<property name="color" stdset="0">
<property name="color">
<color>
<red>85</red>
<green>123</green>
@@ -165,12 +165,12 @@
<property name="toolTip">
<string>Style sheet how user interface will look like</string>
</property>
<property name="prefPath" stdset="0">
<cstring>MainWindow</cstring>
</property>
<property name="prefEntry" stdset="0">
<cstring>StyleSheet</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>MainWindow</cstring>
</property>
<property name="prefType" stdset="0">
<cstring></cstring>
</property>
@@ -178,12 +178,12 @@
</item>
<item row="4" column="1">
<widget class="Gui::PrefComboBox" name="OverlayStyleSheets">
<property name="prefPath" stdset="0">
<cstring>MainWindow</cstring>
</property>
<property name="prefEntry" stdset="0">
<cstring>OverlayActiveStyleSheet</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>MainWindow</cstring>
</property>
<property name="prefType" stdset="0">
<cstring></cstring>
</property>
@@ -191,6 +191,13 @@
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="themeEditorButton">
<property name="text">
<string>Open Theme Editor</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@@ -230,12 +237,12 @@
<property name="value">
<number>16</number>
</property>
<property name="prefPath" stdset="0">
<cstring>TreeView</cstring>
</property>
<property name="prefEntry" stdset="0">
<cstring>IconSize</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>TreeView</cstring>
</property>
</widget>
</item>
<item row="1" column="0">
@@ -285,12 +292,12 @@
<property name="value">
<number>0</number>
</property>
<property name="prefPath" stdset="0">
<cstring>TreeView</cstring>
</property>
<property name="prefEntry" stdset="0">
<cstring>ItemSpacing</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>TreeView</cstring>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
@@ -492,7 +499,7 @@
<item>
<spacer name="spacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@@ -510,26 +517,26 @@
<extends>QPushButton</extends>
<header>Gui/Widgets.h</header>
</customwidget>
<customwidget>
<class>Gui::PrefColorButton</class>
<extends>Gui::ColorButton</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
<customwidget>
<class>Gui::PrefComboBox</class>
<extends>QComboBox</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
<customwidget>
<class>Gui::PrefSpinBox</class>
<extends>QSpinBox</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
<customwidget>
<class>Gui::PrefColorButton</class>
<extends>Gui::ColorButton</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
<customwidget>
<class>Gui::PrefCheckBox</class>
<extends>QCheckBox</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
<customwidget>
<class>Gui::PrefComboBox</class>
<extends>QComboBox</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>ThemeAccentColor1</tabstop>
@@ -0,0 +1,353 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <[email protected]> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#include "PreCompiled.h"
#include "ParameterManager.h"
#include "Parser.h"
#ifndef _PreComp_
#include <QColor>
#include <QRegularExpression>
#include <QString>
#include <ranges>
#include <variant>
#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<Length>(*this)) {
auto [value, unit] = std::get<Length>(*this);
return fmt::format("{}{}", value, unit);
}
if (std::holds_alternative<QColor>(*this)) {
auto color = std::get<QColor>(*this);
return fmt::format("#{:0>6x}", 0xFFFFFF & color.rgb()); // NOLINT(*-magic-numbers)
}
return std::get<std::string>(*this);
}
ParameterSource::ParameterSource(const Metadata& metadata)
: metadata(metadata)
{}
InMemoryParameterSource::InMemoryParameterSource(const std::list<Parameter>& parameters,
const Metadata& metadata)
: ParameterSource(metadata)
{
for (const auto& parameter : parameters) {
InMemoryParameterSource::define(parameter);
}
}
std::list<Parameter> InMemoryParameterSource::all() const
{
auto values = parameters | std::ranges::views::values;
return std::list<Parameter>(values.begin(), values.end());
}
std::optional<Parameter> 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<Parameter> BuiltInParameterSource::all() const
{
std::list<Parameter> result;
for (const auto& name : params | std::views::keys) {
result.push_back(*get(name));
}
return result;
}
std::optional<Parameter> 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<Parameter> UserParameterSource::all() const
{
std::list<Parameter> result;
for (const auto& [token, value] : hGrp->GetASCIIMap()) {
result.push_back({
.name = token,
.value = value,
});
}
return result;
}
std::optional<Parameter> 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<QString(const QRegularExpressionMatch&)>& 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<Parameter> ParameterManager::parameters() const
{
std::set<Parameter, Parameter::NameComparator> 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<std::string> 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<Parameter> 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<Parameter> 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<ParameterSource*> ParameterManager::sources() const
{
return _sources;
}
} // namespace Gui::StyleParameters
+457
View File
@@ -0,0 +1,457 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <[email protected]> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#ifndef STYLEPARAMETERS_PARAMETERMANAGER_H
#define STYLEPARAMETERS_PARAMETERMANAGER_H
#include <list>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <vector>
#include <QColor>
#include <App/Application.h>
#include <Base/Bitmask.h>
#include <Base/Parameter.h>
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<Length, QColor, std::string>
{
using std::variant<Length, QColor, std::string>::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<ParameterSourceOption>;
/**
* @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<Parameter> 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<Parameter> 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<std::string, Parameter> parameters;
public:
InMemoryParameterSource(const std::list<Parameter>& parameters, const Metadata& metadata);
std::list<Parameter> all() const override;
std::optional<Parameter> 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<Parameter> all() const override;
std::optional<Parameter> 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<std::string, ParameterGrp::handle> 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<Parameter> all() const override;
std::optional<Parameter> 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<ParameterSource*> _sources;
mutable std::map<std::string, Value> _resolved;
public:
struct ResolveContext
{
/// Names of parameters currently being resolved.
std::set<std::string> 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<Parameter> 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<std::string> 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> 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<ParameterSource*> sources() const;
};
} // namespace Gui::StyleParameters
ENABLE_BITMASK_OPERATORS(Gui::StyleParameters::ParameterSourceOption);
#endif // STYLEPARAMETERS_PARAMETERMANAGER_H
+403
View File
@@ -0,0 +1,403 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <[email protected]> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#include "PreCompiled.h"
#include "Parser.h"
#include "ParameterManager.h"
#ifndef _PreComp_
#include <QColor>
#include <QRegularExpression>
#include <QString>
#include <ranges>
#include <variant>
#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<QColor>(colorArg)) {
THROWM(Base::ExpressionError,
fmt::format("'{}' is not supported for colors", functionName));
}
auto color = std::get<QColor>(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<int>(std::get<Length>(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<Length>(lval) || !std::holds_alternative<Length>(rval)) {
THROWM(Base::ExpressionError, "Math operations are supported only on lengths");
}
auto lhs = std::get<Length>(lval);
auto rhs = std::get<Length>(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<QColor>(val)) {
THROWM(Base::ExpressionError, "Unary operations on colors are not supported");
}
auto v = std::get<Length>(val);
switch (op) {
case Operator::Add:
return v;
case Operator::Subtract:
return -v;
default:
THROWM(Base::ExpressionError, "Unknown unary operator");
}
}
std::unique_ptr<Expr> 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<Expr> Parser::parseExpression()
{
auto expr = parseTerm();
while (true) {
skipWhitespace();
if (match('+')) {
expr = std::make_unique<BinaryOp>(std::move(expr), Operator::Add, parseTerm());
}
else if (match('-')) {
expr = std::make_unique<BinaryOp>(std::move(expr), Operator::Subtract, parseTerm());
}
else {
break;
}
}
return expr;
}
std::unique_ptr<Expr> Parser::parseTerm()
{
auto expr = parseFactor();
while (true) {
skipWhitespace();
if (match('*')) {
expr = std::make_unique<BinaryOp>(std::move(expr), Operator::Multiply, parseFactor());
}
else if (match('/')) {
expr = std::make_unique<BinaryOp>(std::move(expr), Operator::Divide, parseFactor());
}
else {
break;
}
}
return expr;
}
std::unique_ptr<Expr> Parser::parseFactor()
{
skipWhitespace();
if (match('+') || match('-')) {
Operator op = (input[pos - 1] == '+') ? Operator::Add : Operator::Subtract;
return std::make_unique<UnaryOp>(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<Expr> 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<Color>(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<Color>(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<Expr> 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<ParameterReference>(input.substr(start, pos - start));
}
bool Parser::peekFunction()
{
skipWhitespace();
return pos < input.size() && isalpha(input[pos]);
}
std::unique_ptr<Expr> 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<std::unique_ptr<Expr>> 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<FunctionCall>(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<Expr> 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<Number>(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
+170
View File
@@ -0,0 +1,170 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <[email protected]> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#ifndef STYLEPARAMETERS_PARSER_H
#define STYLEPARAMETERS_PARSER_H
#include <memory>
#include <string>
#include <vector>
#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<std::unique_ptr<Expr>> arguments;
FunctionCall(std::string functionName, std::vector<std::unique_ptr<Expr>> arguments)
: functionName(std::move(functionName))
, arguments(std::move(arguments))
{}
Value evaluate(const EvaluationContext& context) const override;
};
struct GuiExport BinaryOp: public Expr
{
std::unique_ptr<Expr> left, right;
Operator op;
BinaryOp(std::unique_ptr<Expr> left, Operator op, std::unique_ptr<Expr> 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<Expr> operand;
UnaryOp(Operator op, std::unique_ptr<Expr> 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<Expr> parse();
private:
bool peekString(const char* function) const;
std::unique_ptr<Expr> parseExpression();
std::unique_ptr<Expr> parseTerm();
std::unique_ptr<Expr> parseFactor();
bool peekColor();
std::unique_ptr<Expr> parseColor();
bool peekParameter();
std::unique_ptr<Expr> parseParameter();
bool peekFunction();
std::unique_ptr<Expr> parseFunctionCall();
int parseInt();
std::unique_ptr<Expr> parseNumber();
std::string parseUnit();
bool match(char expected);
void skipWhitespace();
};
} // namespace Gui::StyleParameters
#endif // STYLEPARAMETERS_PARSER_H
+86 -24
View File
@@ -1,5 +1,6 @@
/***************************************************************************
* Copyright (c) 2020 Werner Mayer <wmayer[at]users.sourceforge.net> *
* Copyright (c) 2025 Kacper Donat <[email protected]> *
* *
* This file is part of the FreeCAD CAx development system. *
* *
@@ -23,39 +24,100 @@
#ifndef GUI_TOOLS_H
#define GUI_TOOLS_H
#include <FCGlobal.h>
#include <QFontMetrics>
#include <QKeyEvent>
#include <QKeySequence>
#include <FCGlobal.h>
#include <QModelIndex>
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<typename Func>
concept TreeWalkCallable =
std::is_invocable_r_v<void, Func, const QModelIndex&> ||
std::is_invocable_r_v<bool, Func, const QModelIndex&>;
// 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<decltype(func), const QModelIndex&>;
if (index.isValid()) {
if constexpr (std::is_same_v<ReturnType, void>) {
func(index);
}
else if constexpr (std::is_same_v<ReturnType, bool>) {
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<typename T>
T valueOr(const QVariant& variant, const T& defaultValue)
{
return variant.canConvert<T>() ? variant.value<T>() : defaultValue;
}
} // namespace Gui::QtTools
#endif // GUI_TOOLS_H
+5 -2
View File
@@ -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<int>(sampleIcon.width() / pixelRatio);
}
if(!isLinked())
return QIcon();
+3 -3
View File
@@ -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;
+44 -11
View File
@@ -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
return obj
+38 -32
View File
@@ -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
+17 -16
View File
@@ -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):
+5 -2
View File
@@ -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()
+1 -1
View File
@@ -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):
+4 -11
View File
@@ -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()
+3 -4
View File
@@ -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):
+10
View File
@@ -113,6 +113,16 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkLoadTypes">
<property name="toolTip">
<string>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</string>
</property>
<property name="text">
<string>Preload types</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkLoadPsets">
<property name="toolTip">
@@ -406,9 +406,6 @@
<property name="toolTip">
<string>The main use of this building</string>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLength</enum>
</property>
<property name="minimumContentsLength">
<number>0</number>
</property>
@@ -129,6 +129,22 @@
</property>
</widget>
</item>
<item>
<widget class="Gui::PrefCheckBox" name="checkBox_9">
<property name="toolTip">
<string>Load all types automatically when opening an IFC file</string>
</property>
<property name="text">
<string>Preload types</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>LoadTypes</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/NativeIFC</cstring>
</property>
</widget>
</item>
<item>
<widget class="Gui::PrefCheckBox" name="checkBox_8">
<property name="toolTip">
+36 -35
View File
@@ -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())
+53 -47
View File
@@ -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):
+7
View File
@@ -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))
+1
View File
@@ -350,6 +350,7 @@ def create_children(
]
for window in windows:
subresult.extend(create_child(child, window))
if recursive:
subresult.extend(
create_children(
+29 -2
View File
@@ -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
+59 -47
View File
@@ -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):
+12 -18
View File
@@ -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
+40 -45
View File
@@ -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):
+1
View File
@@ -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),
@@ -274,14 +274,14 @@
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="1" column="0">
<widget class="QLabel" name="l_box_height">
<widget class="QLabel" name="l_box_width">
<property name="text">
<string>Height</string>
<string>Width</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="Gui::QuantitySpinBox" name="qsb_box_height">
<widget class="Gui::QuantitySpinBox" name="qsb_box_width">
<property name="alignment">
<set>Qt::AlignLeft|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
@@ -306,14 +306,14 @@
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="l_box_width">
<widget class="QLabel" name="l_box_height">
<property name="text">
<string>Width</string>
<string>Height</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="Gui::QuantitySpinBox" name="qsb_box_width">
<widget class="Gui::QuantitySpinBox" name="qsb_box_height">
<property name="alignment">
<set>Qt::AlignLeft|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
+2 -2
View File
@@ -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,
+3
View File
@@ -22,6 +22,9 @@
<property name="text">
<string>Create solid</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
+3
View File
@@ -52,6 +52,9 @@
<property name="text">
<string>Create solid</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="1">
+1 -1
View File
@@ -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);
}
+1 -1
View File
@@ -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());
+1 -1
View File
@@ -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<max; i++) {
for (int j=0; j<max; j++) {
+1 -1
View File
@@ -215,6 +215,6 @@ PyMOD_INIT_FUNC(SandboxGui)
// the following constructor call registers our extension module
// with the Python runtime system
PyObject* mod = SandboxGui::initModule();
Base::Console().log("Loading GUI of Sandbox module... done\n");
Base::Console().log("Loading GUI of Sandbox module done\n");
PyMOD_Return(mod);
}
+75 -75
View File
@@ -90,10 +90,10 @@ CmdSandboxDocumentThread::CmdSandboxDocumentThread()
{
sAppModule = "Sandbox";
sGroup = QT_TR_NOOP("Sandbox");
sMenuText = QT_TR_NOOP("Run several threads");
sToolTipText = QT_TR_NOOP("Sandbox Test function");
sMenuText = QT_TR_NOOP("Run Several Threads");
sToolTipText = QT_TR_NOOP("Runs a sandbox test function");
sWhatsThis = "Sandbox_Thread";
sStatusTip = QT_TR_NOOP("Sandbox Test function");
sStatusTip = sToolTipText;
sPixmap = "Std_Tool1";
}
@@ -117,10 +117,10 @@ CmdSandboxDocumentTestThread::CmdSandboxDocumentTestThread()
{
sAppModule = "Sandbox";
sGroup = QT_TR_NOOP("Sandbox");
sMenuText = QT_TR_NOOP("Test thread");
sToolTipText = QT_TR_NOOP("Sandbox Test function");
sMenuText = QT_TR_NOOP("Test Thread");
sToolTipText = QT_TR_NOOP("Runs a sandbox test function");
sWhatsThis = "Sandbox_TestThread";
sStatusTip = QT_TR_NOOP("Sandbox Test function");
sStatusTip = sToolTipText;
sPixmap = "Std_Tool1";
}
@@ -141,10 +141,10 @@ CmdSandboxDocumentSaveThread::CmdSandboxDocumentSaveThread()
{
sAppModule = "Sandbox";
sGroup = QT_TR_NOOP("Sandbox");
sMenuText = QT_TR_NOOP("Save thread");
sToolTipText = QT_TR_NOOP("Sandbox save function");
sMenuText = QT_TR_NOOP("Save Thread");
sToolTipText = QT_TR_NOOP("Runs a sandbox save function");
sWhatsThis = "Sandbox_SaveThread";
sStatusTip = QT_TR_NOOP("Sandbox save function");
sStatusTip = sToolTipText;
}
void CmdSandboxDocumentSaveThread::activated(int)
@@ -169,10 +169,10 @@ CmdSandboxDocThreadWithSeq::CmdSandboxDocThreadWithSeq()
{
sAppModule = "Sandbox";
sGroup = QT_TR_NOOP("Sandbox");
sMenuText = QT_TR_NOOP("Thread and sequencer");
sToolTipText = QT_TR_NOOP("Sandbox Test function");
sMenuText = QT_TR_NOOP("Thread and Sequencer");
sToolTipText = QT_TR_NOOP("Runs a sandbox test function");
sWhatsThis = "Sandbox_SeqThread";
sStatusTip = QT_TR_NOOP("Sandbox Test function");
sStatusTip = sToolTipText;
sPixmap = "Std_Tool2";
}
@@ -188,7 +188,7 @@ void CmdSandboxDocThreadWithSeq::activated(int)
#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<max; i++) {
for (int j=0; j<max; j++) {
@@ -208,10 +208,10 @@ CmdSandboxDocThreadBusy::CmdSandboxDocThreadBusy()
{
sAppModule = "Sandbox";
sGroup = QT_TR_NOOP("Sandbox");
sMenuText = QT_TR_NOOP("Thread and no sequencer");
sToolTipText = QT_TR_NOOP("Sandbox Test function");
sMenuText = QT_TR_NOOP("Thread and No Sequencer");
sToolTipText = QT_TR_NOOP("Runs a sandbox test function");
sWhatsThis = "Sandbox_BlockThread";
sStatusTip = QT_TR_NOOP("Sandbox Test function");
sStatusTip = sToolTipText;
sPixmap = "Std_Tool3";
}
@@ -245,10 +245,10 @@ CmdSandboxDocumentNoThread::CmdSandboxDocumentNoThread()
{
sAppModule = "Sandbox";
sGroup = QT_TR_NOOP("Sandbox");
sMenuText = QT_TR_NOOP("GUI thread");
sToolTipText = QT_TR_NOOP("Sandbox Test function");
sMenuText = QT_TR_NOOP("GUI Thread");
sToolTipText = QT_TR_NOOP("Runs a sandbox test function");
sWhatsThis = "Sandbox_NoThread";
sStatusTip = QT_TR_NOOP("Sandbox Test function");
sStatusTip = sToolTipText;
sPixmap = "Std_Tool4";
}
@@ -276,10 +276,10 @@ CmdSandboxWorkerThread::CmdSandboxWorkerThread()
{
sAppModule = "Sandbox";
sGroup = QT_TR_NOOP("Sandbox");
sMenuText = QT_TR_NOOP("Worker thread");
sToolTipText = QT_TR_NOOP("Sandbox Test function");
sMenuText = QT_TR_NOOP("Worker Thread");
sToolTipText = QT_TR_NOOP("Runs a sandbox test function");
sWhatsThis = "Sandbox_WorkerThread";
sStatusTip = QT_TR_NOOP("Sandbox Test function");
sStatusTip = sToolTipText;
sPixmap = "Std_Tool1";
}
@@ -299,10 +299,10 @@ CmdSandboxPythonLockThread::CmdSandboxPythonLockThread()
{
sAppModule = "Sandbox";
sGroup = QT_TR_NOOP("Sandbox");
sMenuText = QT_TR_NOOP("Locked Python threads");
sToolTipText = QT_TR_NOOP("Use Python's thread module where each thread is locked");
sMenuText = QT_TR_NOOP("Locked Python Threads");
sToolTipText = QT_TR_NOOP("Uses Python's thread module where each thread is locked");
sWhatsThis = "Sandbox_PythonLockThread";
sStatusTip = QT_TR_NOOP("Use Python's thread module where each thread is locked");
sStatusTip = sToolTipText;
}
void CmdSandboxPythonLockThread::activated(int)
@@ -334,10 +334,10 @@ CmdSandboxPythonNolockThread::CmdSandboxPythonNolockThread()
{
sAppModule = "Sandbox";
sGroup = QT_TR_NOOP("Sandbox");
sMenuText = QT_TR_NOOP("Unlocked Python threads");
sToolTipText = QT_TR_NOOP("Use Python's thread module where each thread is unlocked");
sMenuText = QT_TR_NOOP("Unlocked Python Threads");
sToolTipText = QT_TR_NOOP("Uses Python's thread module where each thread is unlocked");
sWhatsThis = "Sandbox_NolockPython";
sStatusTip = QT_TR_NOOP("Use Python's thread module where each thread is unlocked");
sStatusTip = sToolTipText;
}
void CmdSandboxPythonNolockThread::activated(int)
@@ -366,10 +366,10 @@ CmdSandboxPySideThread::CmdSandboxPySideThread()
{
sAppModule = "Sandbox";
sGroup = QT_TR_NOOP("Sandbox");
sMenuText = QT_TR_NOOP("PySide threads");
sToolTipText = QT_TR_NOOP("Use PySide's thread module");
sMenuText = QT_TR_NOOP("PySide Threads");
sToolTipText = QT_TR_NOOP("Uses PySide's thread module");
sWhatsThis = "Sandbox_PySideThread";
sStatusTip = QT_TR_NOOP("Use PySide's thread module");
sStatusTip = sToolTipText;
}
void CmdSandboxPySideThread::activated(int)
@@ -401,10 +401,10 @@ CmdSandboxPythonThread::CmdSandboxPythonThread()
{
sAppModule = "Sandbox";
sGroup = QT_TR_NOOP("Sandbox");
sMenuText = QT_TR_NOOP("Python threads");
sToolTipText = QT_TR_NOOP("Use class PythonThread running Python code in its run() method");
sMenuText = QT_TR_NOOP("Python Threads");
sToolTipText = QT_TR_NOOP("Uses class PythonThread running Python code in its run() method");
sWhatsThis = "Sandbox_PythonThread";
sStatusTip = QT_TR_NOOP("Use class PythonThread running Python code in its run() method");
sStatusTip = sToolTipText;
}
void CmdSandboxPythonThread::activated(int)
@@ -427,10 +427,10 @@ CmdSandboxPythonMainThread::CmdSandboxPythonMainThread()
{
sAppModule = "Sandbox";
sGroup = QT_TR_NOOP("Sandbox");
sMenuText = QT_TR_NOOP("Python main thread");
sToolTipText = QT_TR_NOOP("Run python code in main thread");
sMenuText = QT_TR_NOOP("Python Main Thread");
sToolTipText = QT_TR_NOOP("Runs Python code in the main thread");
sWhatsThis = "Sandbox_PythonMainThread";
sStatusTip = QT_TR_NOOP("Run python code in main thread");
sStatusTip = sToolTipText;
}
void CmdSandboxPythonMainThread::activated(int)
@@ -453,10 +453,10 @@ CmdSandboxDocThreadWithDialog::CmdSandboxDocThreadWithDialog()
{
sAppModule = "Sandbox";
sGroup = QT_TR_NOOP("Sandbox");
sMenuText = QT_TR_NOOP("Thread and modal dialog");
sToolTipText = QT_TR_NOOP("Sandbox Test function");
sMenuText = QT_TR_NOOP("Thread and Modal Dialog");
sToolTipText = QT_TR_NOOP("Runs a sandbox test function");
sWhatsThis = "Sandbox_Dialog";
sStatusTip = QT_TR_NOOP("Sandbox Test function");
sStatusTip = sToolTipText;
sPixmap = "Std_Tool7";
}
@@ -480,10 +480,10 @@ CmdSandboxDocThreadWithFileDlg::CmdSandboxDocThreadWithFileDlg()
{
sAppModule = "Sandbox";
sGroup = QT_TR_NOOP("Sandbox");
sMenuText = QT_TR_NOOP("Thread and file dialog");
sToolTipText = QT_TR_NOOP("Sandbox Test function");
sMenuText = QT_TR_NOOP("Thread and File Dialog");
sToolTipText = QT_TR_NOOP("Runs a sandbox test function");
sWhatsThis = "Sandbox_FileDialog";
sStatusTip = QT_TR_NOOP("Sandbox Test function");
sStatusTip = sToolTipText;
sPixmap = "Std_Tool7";
}
@@ -518,10 +518,10 @@ CmdSandboxEventLoop::CmdSandboxEventLoop()
{
sAppModule = "Sandbox";
sGroup = QT_TR_NOOP("Sandbox");
sMenuText = QT_TR_NOOP("Local event loop");
sToolTipText = QT_TR_NOOP("Sandbox Test function");
sMenuText = QT_TR_NOOP("Local Event Loop");
sToolTipText = QT_TR_NOOP("Runs a sandbox test function");
sWhatsThis = "Sandbox_EventLoop";
sStatusTip = QT_TR_NOOP("Sandbox Test function");
sStatusTip = sToolTipText;
sPixmap = "Std_Tool6";
}
@@ -561,10 +561,10 @@ CmdSandboxMeshLoader::CmdSandboxMeshLoader()
{
sAppModule = "Sandbox";
sGroup = QT_TR_NOOP("Sandbox");
sMenuText = QT_TR_NOOP("Load mesh in thread");
sToolTipText = QT_TR_NOOP("Sandbox Test function");
sMenuText = QT_TR_NOOP("Load Mesh in Thread");
sToolTipText = QT_TR_NOOP("Runs a sandbox test function");
sWhatsThis = "Sandbox_MeshLoad";
sStatusTip = QT_TR_NOOP("Sandbox Test function");
sStatusTip = sToolTipText;
sPixmap = "Std_Tool6";
}
@@ -619,10 +619,10 @@ CmdSandboxMeshLoaderBoost::CmdSandboxMeshLoaderBoost()
{
sAppModule = "Sandbox";
sGroup = QT_TR_NOOP("Sandbox");
sMenuText = QT_TR_NOOP("Load mesh in boost-thread");
sToolTipText = QT_TR_NOOP("Sandbox Test function");
sMenuText = QT_TR_NOOP("Load Mesh in Boost-Thread");
sToolTipText = QT_TR_NOOP("Runs a sandbox test function");
sWhatsThis = "Sandbox_MeshLoaderBoost";
sStatusTip = QT_TR_NOOP("Sandbox Test function");
sStatusTip = sToolTipText;
sPixmap = "Std_Tool6";
}
@@ -667,10 +667,10 @@ CmdSandboxMeshLoaderFuture::CmdSandboxMeshLoaderFuture()
{
sAppModule = "Sandbox";
sGroup = QT_TR_NOOP("Sandbox");
sMenuText = QT_TR_NOOP("Load mesh in QFuture");
sToolTipText = QT_TR_NOOP("Sandbox Test function");
sMenuText = QT_TR_NOOP("Load Mesh in QFuture");
sToolTipText = QT_TR_NOOP("Runs a sandbox test function");
sWhatsThis = "Sandbox_MeshLoaderFuture";
sStatusTip = QT_TR_NOOP("Sandbox Test function");
sStatusTip = sToolTipText;
sPixmap = "Std_Tool6";
}
@@ -782,10 +782,10 @@ CmdSandboxMeshTestJob::CmdSandboxMeshTestJob()
{
sAppModule = "Sandbox";
sGroup = QT_TR_NOOP("Sandbox");
sMenuText = QT_TR_NOOP("Test mesh job");
sToolTipText = QT_TR_NOOP("Sandbox Test function");
sMenuText = QT_TR_NOOP("Test Mesh Job");
sToolTipText = QT_TR_NOOP("Runs a sandbox test function");
sWhatsThis = "Sandbox_MeshTestJob";
sStatusTip = QT_TR_NOOP("Sandbox Test function");
sStatusTip = sToolTipText;
sPixmap = "Std_Tool7";
}
@@ -822,7 +822,7 @@ void CmdSandboxMeshTestJob::activated(int)
}
// run the actual multi-threaded mesh test
Base::Console().message("Mesh test (step %d)...\n",iteration++);
Base::Console().message("Mesh test (step %d)\n",iteration++);
MeshTestJob meshJob;
QFuture<Mesh::MeshObject*> 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;
+4 -3
View File
@@ -8632,9 +8632,6 @@ void SketchObject::rebuildExternalGeometry(std::optional<ExternalToAdd> 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<ExternalToAdd> extToAdd
BRepBuilderAPI_MakeFace mkFace(sketchPlane);
TopoDS_Shape aProjFace = mkFace.Shape();
if (Types.size() != Objects.size()) {
Types.resize(Objects.size(), 0);
}
std::set<std::string> refSet;
// We use a vector here to keep the order (roughly) the same as ExternalGeometry
std::vector<std::vector<std::unique_ptr<Part::Geometry> > > newGeos;
+23 -23
View File
@@ -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*/)
@@ -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;
+14 -4
View File
@@ -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());
+6 -15
View File
@@ -293,21 +293,12 @@ This setting is only for the toolbar. Whichever you choose, all tools are always
</item>
<item row="2" column="1">
<widget class="QComboBox" name="autoScaleMode">
<item>
<property name="text">
<string>Always</string>
</property>
</item>
<item>
<property name="text">
<string>Never</string>
</property>
</item>
<item>
<property name="text">
<string>Only if there is no visual scale indicator</string>
</property>
</item>
<property name="toolTip">
<string>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.</string>
</property>
</widget>
</item>
</layout>
+2 -2
View File
@@ -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 =
+3
View File
@@ -2,6 +2,9 @@
target_sources(Tests_run PRIVATE
Assistant.cpp
Camera.cpp
StyleParameters/StyleParametersApplicationTest.cpp
StyleParameters/ParserTest.cpp
StyleParameters/ParameterManagerTest.cpp
)
# Qt tests
@@ -0,0 +1,326 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <kacper@kadet.net> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#include <gtest/gtest.h>
#include <Gui/Application.h>
#include <Gui/StyleParameters/ParameterManager.h>
using namespace Gui::StyleParameters;
class ParameterManagerTest: public ::testing::Test
{
protected:
void SetUp() override
{
// Create test sources
auto source1 = std::make_unique<InMemoryParameterSource>(
std::list<Parameter> {
{"BaseSize", "8px"},
{"PrimaryColor", "#ff0000"},
{"SecondaryColor", "#00ff00"},
},
ParameterSource::Metadata {"Source 1"});
auto source2 = std::make_unique<InMemoryParameterSource>(
std::list<Parameter> {
{"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<std::unique_ptr<ParameterSource>> sources;
};
// Test basic parameter resolution
TEST_F(ParameterManagerTest, BasicParameterResolution)
{
{
auto result = manager.resolve("BaseSize");
EXPECT_TRUE(std::holds_alternative<Length>(result));
auto length = std::get<Length>(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<QColor>(result));
auto color = std::get<QColor>(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<QColor>(result));
auto color = std::get<QColor>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result1));
// Second resolution should use cached value
auto result2 = manager.resolve("BaseSize");
EXPECT_TRUE(std::holds_alternative<Length>(result2));
// Results should be identical
auto length1 = std::get<Length>(result1);
auto length2 = std::get<Length>(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<Length>(result1));
auto length1 = std::get<Length>(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<Length>(result2));
auto length2 = std::get<Length>(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<InMemoryParameterSource>(
std::list<Parameter> {
{"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<Length>(result));
auto length = std::get<Length>(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<std::string> 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<InMemoryParameterSource>(
std::list<Parameter> {
{"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<std::string>(result));
}
// Test complex expressions
TEST_F(ParameterManagerTest, ComplexExpressions)
{
// Create a source with complex expressions
auto complexSource = std::make_unique<InMemoryParameterSource>(
std::list<Parameter> {
{"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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<QColor>(result));
auto color = std::get<QColor>(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<std::string>(result));
EXPECT_EQ(std::get<std::string>(result), "");
// Test invalid expression
auto invalidSource = std::make_unique<InMemoryParameterSource>(
std::list<Parameter> {
{"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<std::string>(invalidResult));
}
@@ -0,0 +1,617 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <kacper@kadet.net> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#include <gtest/gtest.h>
#include <QColor>
#include <Gui/StyleParameters/Parser.h>
#include <Gui/StyleParameters/ParameterManager.h>
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<InMemoryParameterSource>(
std::list<Parameter> {
{"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<std::unique_ptr<ParameterSource>> 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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<QColor>(result));
auto color = std::get<QColor>(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<QColor>(result));
auto color = std::get<QColor>(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<QColor>(result));
auto color = std::get<QColor>(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<QColor>(result));
auto color = std::get<QColor>(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<QColor>(result));
auto color = std::get<QColor>(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<Length>(result));
auto length = std::get<Length>(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<QColor>(result));
auto color = std::get<QColor>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<QColor>(result));
auto color = std::get<QColor>(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<QColor>(result));
auto color = std::get<QColor>(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<QColor>(result));
auto color = std::get<QColor>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<QColor>(result));
auto color = std::get<QColor>(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<Length>(result));
auto length = std::get<Length>(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<QColor>(result));
auto color = std::get<QColor>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(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<Length>(result));
auto length = std::get<Length>(result);
EXPECT_DOUBLE_EQ(length.value, 10.0); // (10 - 5) * 2 = 5 * 2 = 10
EXPECT_EQ(length.unit, "");
}
}
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <kacper@kadet.net> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#include <gtest/gtest.h>
#include "src/App/InitApplication.h"
#include <Gui/Application.h>
#include <Gui/StyleParameters/ParameterManager.h>
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; }");
}
+1
View File
@@ -106,6 +106,7 @@ TEST_F(PartFeaturesTest, testSweep)
auto _sweep = _doc->addObject<Sweep>();
_sweep->Sections.setValues({_plane1});
_sweep->Spine.setValue(_edge1);
_sweep->Solid.setValue((false));
// Act
_sweep->execute();
TopoShape ts = _sweep->Shape.getShape();