diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml new file mode 100644 index 0000000..5b8a942 --- /dev/null +++ b/.github/workflows/wasm.yml @@ -0,0 +1,164 @@ +# This workflow builds the WebAssembly prototype of the application. +# +# Overview: +# - Checks out shapes-app and EasyApp side by side, the layout the qrc +# and the .pro file expect. +# - Installs the Emscripten SDK and Qt for WebAssembly, both pinned. +# - Regenerates the Qt resource file, so a stale committed qrc cannot +# silently drop QML files from the build. +# - Builds with qmake and uploads the result as an artifact. +# +# The artifact holds a complete static site. Download it, unzip it, serve +# the folder over HTTP and open the .html file. Opening it directly from +# the file system does not work, since browsers refuse to load wasm over +# file:// URLs. +# +# Deployment to GitHub Pages is deliberately not part of this workflow +# yet - see the notes in docs before adding it, since the gh-pages branch +# is owned by the documentation workflow. + +name: WebAssembly prototype build + +on: + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Allow only one concurrent build, cancelling anything queued behind it. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# Qt for WebAssembly requires an exact Emscripten version. A mismatch +# produces undefined symbols at link time. +# Emsdk 3.1.50 is needed for Qt 6.7 +# Emsdk 3.1.56 is needed for Qt 6.8 +# Emsdk 3.1.70 is needed for Qt 6.9 +# Emsdk 4.0.7 is needed for Qt 6.10 and 6.11 +env: + QT_VER: '6.9.0' + QT_VER_NO_DOTS: '690' + EMSDK_VER: '3.1.70' + # Branch of the EasyApp repository holding the EasyApplication QML modules + EASYAPP_REF: selectable_table_view + +jobs: + build-wasm: + # macOS, matching the runner the EasyApp wasm workflow uses. The + # Windows host is not a supported build platform for this: the + # wasm-emscripten mkspec emits Unix commands such as cp and sed. + runs-on: macos-14 + + timeout-minutes: 45 + + steps: + # The qrc and the .pro reach the framework through a relative path, + # '../../EasyApp/src', so the two repositories must sit next to each + # other exactly as they do in a local workspace. + - name: Checkout shapes-app + uses: actions/checkout@v5 + with: + path: shapes-app + + - name: Checkout EasyApp + uses: actions/checkout@v5 + with: + repository: easyscience/EasyApp + ref: ${{ env.EASYAPP_REF }} + path: EasyApp + + - name: Cache emsdk + id: cache-emsdk + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/emsdk + key: ${{ runner.os }}-emsdk-${{ env.EMSDK_VER }} + + - name: Install emsdk + if: steps.cache-emsdk.outputs.cache-hit != 'true' + run: | + set -euo pipefail + git clone https://github.com/emscripten-core/emsdk.git + cd emsdk + ./emsdk install ${{ env.EMSDK_VER }} + ./emsdk activate ${{ env.EMSDK_VER }} + + - name: Cache Qt + id: cache-qt + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/Qt + key: ${{ runner.os }}-qt-${{ env.QT_VER }}-wasm + + # Qt Quick 3D backs the 3D structure viewer, Qt Graphs backs the + # charts. Neither is part of the base WebAssembly package. + - name: Install Qt for WebAssembly + if: steps.cache-qt.outputs.cache-hit != 'true' + run: | + set -euo pipefail + curl -fL -O https://download.qt.io/official_releases/online_installers/qt-online-installer-macOS-universal.dmg + hdiutil attach -nobrowse qt-online-installer-macOS-universal.dmg + # The volume and the executable inside carry the installer version + # in their names, so discover them rather than hard-coding one. + VOLUME=$(ls -d /Volumes/qt-online-installer* | head -1) + INSTALLER=$(find "${VOLUME}" -type f -path '*/Contents/MacOS/*' | head -1) + echo "using ${INSTALLER}" + "${INSTALLER}" \ + --email ${{ secrets.QT_ACCOUNT_EMAIL }} \ + --pw ${{ secrets.QT_ACCOUNT_PASSWORD }} \ + --root ${{ github.workspace }}/Qt \ + --accept-licenses \ + --accept-obligations \ + --default-answer \ + --confirm-command \ + install \ + qt.qt6.${{ env.QT_VER_NO_DOTS }}.wasm_singlethread \ + qt.qt6.${{ env.QT_VER_NO_DOTS }}.addons.qtquick3d \ + qt.qt6.${{ env.QT_VER_NO_DOTS }}.addons.qtgraphs + hdiutil detach "${VOLUME}" + + - name: Print toolchain versions + run: | + set -euo pipefail + source ${{ github.workspace }}/emsdk/emsdk_env.sh + em++ --version + export PATH=$PATH:${{ github.workspace }}/Qt/${{ env.QT_VER }}/wasm_singlethread/bin + qmake -v + + # The committed qrc is generated. Regenerating here means a QML file + # added without re-running the script locally still reaches the build. + - name: Regenerate the Qt resource file + working-directory: shapes-app + run: | + set -euo pipefail + python3 scripts/gen_qrc.py + git --no-pager diff --stat -- src/easyshapes_app.qrc + + - name: Build + working-directory: shapes-app/src + run: | + set -euo pipefail + source ${{ github.workspace }}/emsdk/emsdk_env.sh + export PATH=$PATH:${{ github.workspace }}/Qt/${{ env.QT_VER }}/wasm_singlethread/bin + qmake easyshapes_app.pro -spec wasm-emscripten + make -j$(sysctl -n hw.ncpu) + + # qtloader.js and qtlogo.svg are copied next to the binary by the + # build itself. A favicon keeps the dev server from logging a 404. + - name: Collect the built site + run: | + set -euo pipefail + mkdir -p site + cd shapes-app/src + cp easyshapes_app.html easyshapes_app.js easyshapes_app.wasm ../../site/ + cp qtloader.js qtlogo.svg ../../site/ 2>/dev/null || true + cd ../../site + cp easyshapes_app.html index.html + ls -lh + # Artifact names cannot contain a forward slash, branches can + echo "REF_SLUG=${GITHUB_REF_NAME//\//-}" >> "${GITHUB_ENV}" + + - name: Upload the built site as artifact + uses: ./shapes-app/.github/actions/upload-artifact + with: + name: wasm-prototype_easyshapes-app_${{ env.REF_SLUG }} + path: site/ diff --git a/.gitignore b/.gitignore index f7ce4ac..d06ee94 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,11 @@ node_modules/ # QtCreator *.autosave +.qtcreator/ + +# QtCreator qmake +*.pro.user +*.pro.user.* # QtCreator Qml *.qmlproject.user diff --git a/pixi.lock b/pixi.lock index c68fef6..4bb7870 100644 --- a/pixi.lock +++ b/pixi.lock @@ -5,6 +5,8 @@ environments: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 @@ -78,7 +80,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/36/41/04e2a649058b0713b00d6c9bd22da35618bb157289e05d068e51fddf8d7e/dunamai-1.25.0-py3-none-any.whl - - pypi: git+https://github.com/easyscience/EasyApp.git?rev=develop#dfef95a881c7f517a8f18de160500750f7a9b8fc + - pypi: git+https://github.com/easyscience/EasyApp.git#dfef95a881c7f517a8f18de160500750f7a9b8fc - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl @@ -304,7 +306,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/36/41/04e2a649058b0713b00d6c9bd22da35618bb157289e05d068e51fddf8d7e/dunamai-1.25.0-py3-none-any.whl - - pypi: git+https://github.com/easyscience/EasyApp.git?rev=develop#dfef95a881c7f517a8f18de160500750f7a9b8fc + - pypi: git+https://github.com/easyscience/EasyApp.git#dfef95a881c7f517a8f18de160500750f7a9b8fc - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl @@ -410,9 +412,9 @@ environments: - pypi: https://files.pythonhosted.org/packages/40/6d/b6ee155462a0156b94312bdd82d2b92ea56e909740045a87ccb98bf52405/pymdown_extensions-10.20.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/35/0f/5736889fc850794623692cb369e295a994175e51295fa52134626f486296/pyside6-6.10.2-cp39-abi3-macosx_13_0_universal2.whl - - pypi: https://files.pythonhosted.org/packages/61/06/c283567628ffa2cefc3c72374ad607f1dfc9842a03db65f1347b9ae52bee/pyside6_addons-6.10.2-cp39-abi3-macosx_13_0_universal2.whl - - pypi: https://files.pythonhosted.org/packages/1d/2e/5f18a77f5e0bd730bacec93a690d0ef3c96a9711d213653eacecbf241b8d/pyside6_essentials-6.10.2-cp39-abi3-macosx_13_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/da/a6/27ba5947ed48918f7b74b7c43a1e280aac069e36f25adeb4c9adfac835c4/pyside6-6.11.1-cp310-abi3-macosx_13_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/3f/6b/8bc94aff48b63f788f2d84e5467c12362d68906ba742c0942f46cb04c879/pyside6_addons-6.11.1-cp310-abi3-macosx_13_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/b3/da/10d9197e7370eb4fed8df5fc547b7548dec88e5c5949e2d450db4ae96feb/pyside6_essentials-6.11.1-cp310-abi3-macosx_13_0_universal2.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl @@ -433,7 +435,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl - pypi: https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fb/38/3912eb08a3b865b5fcdb4bdce8076cacc211986cee587f5cb62e637791af/shiboken6-6.10.2-cp39-abi3-macosx_13_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/17/f3/f2b63df0251e7cd3172ea28e32ede52739de9566bcefcd0178681538ac81/shiboken6-6.11.1-cp310-abi3-macosx_13_0_universal2.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl @@ -531,7 +533,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/36/41/04e2a649058b0713b00d6c9bd22da35618bb157289e05d068e51fddf8d7e/dunamai-1.25.0-py3-none-any.whl - - pypi: git+https://github.com/easyscience/EasyApp.git?rev=develop#dfef95a881c7f517a8f18de160500750f7a9b8fc + - pypi: git+https://github.com/easyscience/EasyApp.git#dfef95a881c7f517a8f18de160500750f7a9b8fc - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl @@ -637,9 +639,9 @@ environments: - pypi: https://files.pythonhosted.org/packages/40/6d/b6ee155462a0156b94312bdd82d2b92ea56e909740045a87ccb98bf52405/pymdown_extensions-10.20.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/35/0f/5736889fc850794623692cb369e295a994175e51295fa52134626f486296/pyside6-6.10.2-cp39-abi3-macosx_13_0_universal2.whl - - pypi: https://files.pythonhosted.org/packages/61/06/c283567628ffa2cefc3c72374ad607f1dfc9842a03db65f1347b9ae52bee/pyside6_addons-6.10.2-cp39-abi3-macosx_13_0_universal2.whl - - pypi: https://files.pythonhosted.org/packages/1d/2e/5f18a77f5e0bd730bacec93a690d0ef3c96a9711d213653eacecbf241b8d/pyside6_essentials-6.10.2-cp39-abi3-macosx_13_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/da/a6/27ba5947ed48918f7b74b7c43a1e280aac069e36f25adeb4c9adfac835c4/pyside6-6.11.1-cp310-abi3-macosx_13_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/3f/6b/8bc94aff48b63f788f2d84e5467c12362d68906ba742c0942f46cb04c879/pyside6_addons-6.11.1-cp310-abi3-macosx_13_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/b3/da/10d9197e7370eb4fed8df5fc547b7548dec88e5c5949e2d450db4ae96feb/pyside6_essentials-6.11.1-cp310-abi3-macosx_13_0_universal2.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl @@ -660,7 +662,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/fb/38/3912eb08a3b865b5fcdb4bdce8076cacc211986cee587f5cb62e637791af/shiboken6-6.10.2-cp39-abi3-macosx_13_0_universal2.whl + - pypi: https://files.pythonhosted.org/packages/17/f3/f2b63df0251e7cd3172ea28e32ede52739de9566bcefcd0178681538ac81/shiboken6-6.11.1-cp310-abi3-macosx_13_0_universal2.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl @@ -751,7 +753,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/36/41/04e2a649058b0713b00d6c9bd22da35618bb157289e05d068e51fddf8d7e/dunamai-1.25.0-py3-none-any.whl - - pypi: git+https://github.com/easyscience/EasyApp.git?rev=develop#dfef95a881c7f517a8f18de160500750f7a9b8fc + - pypi: git+https://github.com/easyscience/EasyApp.git#dfef95a881c7f517a8f18de160500750f7a9b8fc - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl @@ -855,9 +857,9 @@ environments: - pypi: https://files.pythonhosted.org/packages/40/6d/b6ee155462a0156b94312bdd82d2b92ea56e909740045a87ccb98bf52405/pymdown_extensions-10.20.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/4f/d4/673b8112b4a260377f760be835c4e357163fdaf68a56a1aec59aeb8e584b/pyside6-6.10.2-cp39-abi3-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/99/13/503bec9201881968c372cb634069535e80aec2489f3907d676e151a1023f/pyside6_addons-6.10.2-cp39-abi3-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/22/a8/616bbbd009efd3e17bf9a2db09d90c6764c010565cd2bdea2a240bfd18f7/pyside6_essentials-6.10.2-cp39-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/57/f2/d9d8ce1373dabb37e5919f63cd18446556079631d3f2eea3ada03c29f6b8/pyside6-6.11.1-cp310-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/9a/bd/8adc4d350b3b363f3dfc8fccdcf5bfed25f7e36c2fff30c64e106f4f1572/pyside6_addons-6.11.1-cp310-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/64/0e/b663ecc96ca57b5c91b83b6615d6b174380b0faf30338125c26e053d6aa7/pyside6_essentials-6.11.1-cp310-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl @@ -881,7 +883,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/71/5d/5ca52c0ef86b3d01572131b6709bd531a080995f7e680720e9424328ce1d/shiboken6-6.10.2-cp39-abi3-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/52/b5/3f6fb2ee65b534193fb4ef713dd619dc31dadff5d12c16979a7699ad58be/shiboken6-6.11.1-cp310-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl @@ -1390,7 +1392,7 @@ packages: - importlib-metadata>=1.6.0 ; python_full_version < '3.8' - packaging>=20.9 requires_python: '>=3.5' -- pypi: git+https://github.com/easyscience/EasyApp.git?rev=develop#dfef95a881c7f517a8f18de160500750f7a9b8fc +- pypi: git+https://github.com/easyscience/EasyApp.git#dfef95a881c7f517a8f18de160500750f7a9b8fc name: easyapp version: 0.8.0 requires_dist: @@ -1398,7 +1400,7 @@ packages: requires_python: '>=3.11' - pypi: ./ name: easyshapes-app - version: 999.0.0+dev9 + version: 999.0.0+devdirty35 sha256: ee8e6e8d672def32f03460f6991f1d5120d46ae93c85c2ead776e4e1daedf1d1 requires_dist: - darkdetect @@ -1438,7 +1440,6 @@ packages: - validate-pyproject[all] ; extra == 'dev' - versioningit ; extra == 'dev' requires_python: '>=3.11' - editable: true - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl name: execnet version: 2.1.2 @@ -4886,24 +4887,26 @@ packages: - pyside6-essentials==6.9.3 - pyside6-addons==6.9.3 requires_python: '>=3.9,<3.14' -- pypi: https://files.pythonhosted.org/packages/35/0f/5736889fc850794623692cb369e295a994175e51295fa52134626f486296/pyside6-6.10.2-cp39-abi3-macosx_13_0_universal2.whl +- pypi: https://files.pythonhosted.org/packages/57/f2/d9d8ce1373dabb37e5919f63cd18446556079631d3f2eea3ada03c29f6b8/pyside6-6.11.1-cp310-abi3-win_amd64.whl name: pyside6 - version: 6.10.2 - sha256: 4b084293caa7845d0064aaf6af258e0f7caae03a14a33537d0a552131afddaf0 - requires_dist: - - shiboken6==6.10.2 - - pyside6-essentials==6.10.2 - - pyside6-addons==6.10.2 - requires_python: '>=3.9,<3.15' -- pypi: https://files.pythonhosted.org/packages/4f/d4/673b8112b4a260377f760be835c4e357163fdaf68a56a1aec59aeb8e584b/pyside6-6.10.2-cp39-abi3-win_amd64.whl + version: 6.11.1 + sha256: 0968877ab1fb4ef3587a284da6fe05e8647ada56a6a3750b6395188e01f4aba6 + requires_dist: + - shiboken6==6.11.1 + - pyside6-essentials==6.11.1 + - pyside6-addons==6.11.1 + - tomli>=2.0.1 ; python_full_version < '3.11' + requires_python: '>=3.10,<3.15' +- pypi: https://files.pythonhosted.org/packages/da/a6/27ba5947ed48918f7b74b7c43a1e280aac069e36f25adeb4c9adfac835c4/pyside6-6.11.1-cp310-abi3-macosx_13_0_universal2.whl name: pyside6 - version: 6.10.2 - sha256: 032bad6b18a17fcbf4dddd0397f49b07f8aae7f1a45b7e4de7037bf7fd6e0edf - requires_dist: - - shiboken6==6.10.2 - - pyside6-essentials==6.10.2 - - pyside6-addons==6.10.2 - requires_python: '>=3.9,<3.15' + version: 6.11.1 + sha256: 537682c3b7530817203e667c1f5a2f00486b37bf52c52eeab438544c7a0917f6 + requires_dist: + - shiboken6==6.11.1 + - pyside6-essentials==6.11.1 + - pyside6-addons==6.11.1 + - tomli>=2.0.1 ; python_full_version < '3.11' + requires_python: '>=3.10,<3.15' - pypi: https://files.pythonhosted.org/packages/17/fe/d5c67665f866b8859d02aa1a859f101a1b2fd348cb61746a3e16fd98fb20/pyside6_addons-6.9.3-cp39-abi3-manylinux_2_28_x86_64.whl name: pyside6-addons version: 6.9.3 @@ -4912,22 +4915,22 @@ packages: - shiboken6==6.9.3 - pyside6-essentials==6.9.3 requires_python: '>=3.9,<3.14' -- pypi: https://files.pythonhosted.org/packages/61/06/c283567628ffa2cefc3c72374ad607f1dfc9842a03db65f1347b9ae52bee/pyside6_addons-6.10.2-cp39-abi3-macosx_13_0_universal2.whl +- pypi: https://files.pythonhosted.org/packages/3f/6b/8bc94aff48b63f788f2d84e5467c12362d68906ba742c0942f46cb04c879/pyside6_addons-6.11.1-cp310-abi3-macosx_13_0_universal2.whl name: pyside6-addons - version: 6.10.2 - sha256: 0de7d0c9535e17d5e3b634b61314a1867f3b0f6d35c3d7cdc99efc353192faff + version: 6.11.1 + sha256: 54733c77f789bef5f03c6aff4ad3bec8b2eff021f0cfcbc53d5e6c250ded24f9 requires_dist: - - shiboken6==6.10.2 - - pyside6-essentials==6.10.2 - requires_python: '>=3.9,<3.15' -- pypi: https://files.pythonhosted.org/packages/99/13/503bec9201881968c372cb634069535e80aec2489f3907d676e151a1023f/pyside6_addons-6.10.2-cp39-abi3-win_amd64.whl + - shiboken6==6.11.1 + - pyside6-essentials==6.11.1 + requires_python: '>=3.10,<3.15' +- pypi: https://files.pythonhosted.org/packages/9a/bd/8adc4d350b3b363f3dfc8fccdcf5bfed25f7e36c2fff30c64e106f4f1572/pyside6_addons-6.11.1-cp310-abi3-win_amd64.whl name: pyside6-addons - version: 6.10.2 - sha256: c20150068525a17494f3b6576c5d61c417cf9a5870659e29f5ebd83cd20a78ea + version: 6.11.1 + sha256: 0d13c4dfd671b050a48e4f8d8ddc724b7248f9c0437e7fc47fdf316278572923 requires_dist: - - shiboken6==6.10.2 - - pyside6-essentials==6.10.2 - requires_python: '>=3.9,<3.15' + - shiboken6==6.11.1 + - pyside6-essentials==6.11.1 + requires_python: '>=3.10,<3.15' - pypi: https://files.pythonhosted.org/packages/85/e8/9396cf11a60f80175bb3c5c1d498d84e87b7af653ab4ea001acf821a3981/pyside6_essentials-6.9.3-cp39-abi3-manylinux_2_28_x86_64.whl name: pyside6-essentials version: 6.9.3 @@ -4935,20 +4938,20 @@ packages: requires_dist: - shiboken6==6.9.3 requires_python: '>=3.9,<3.14' -- pypi: https://files.pythonhosted.org/packages/1d/2e/5f18a77f5e0bd730bacec93a690d0ef3c96a9711d213653eacecbf241b8d/pyside6_essentials-6.10.2-cp39-abi3-macosx_13_0_universal2.whl +- pypi: https://files.pythonhosted.org/packages/64/0e/b663ecc96ca57b5c91b83b6615d6b174380b0faf30338125c26e053d6aa7/pyside6_essentials-6.11.1-cp310-abi3-win_amd64.whl name: pyside6-essentials - version: 6.10.2 - sha256: 1dee2cb9803ff135f881dadeb5c0edcef793d1ec4f8a9140a1348cecb71074e1 + version: 6.11.1 + sha256: 63311bd48e32c584599ab04b9ef7c324082374cd2c9fa533f978fb893bb47e40 requires_dist: - - shiboken6==6.10.2 - requires_python: '>=3.9,<3.15' -- pypi: https://files.pythonhosted.org/packages/22/a8/616bbbd009efd3e17bf9a2db09d90c6764c010565cd2bdea2a240bfd18f7/pyside6_essentials-6.10.2-cp39-abi3-win_amd64.whl + - shiboken6==6.11.1 + requires_python: '>=3.10,<3.15' +- pypi: https://files.pythonhosted.org/packages/b3/da/10d9197e7370eb4fed8df5fc547b7548dec88e5c5949e2d450db4ae96feb/pyside6_essentials-6.11.1-cp310-abi3-macosx_13_0_universal2.whl name: pyside6-essentials - version: 6.10.2 - sha256: 0741018c2b6395038cad4c41775cfae3f13a409e87995ac9f7d89e5b1fb6b22a + version: 6.11.1 + sha256: 228de53c2bc26b07e5021fbe3614fc44ca08e4dab9999af08c2b389d2c239957 requires_dist: - - shiboken6==6.10.2 - requires_python: '>=3.9,<3.15' + - shiboken6==6.11.1 + requires_python: '>=3.10,<3.15' - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl name: pytest version: 9.0.2 @@ -5409,16 +5412,16 @@ packages: version: 6.9.3 sha256: f3f5337a3a8fc660ba1462265bd9a2bdda9588f8d90fbc3d5ac4ce3134c11e59 requires_python: '>=3.9,<3.14' -- pypi: https://files.pythonhosted.org/packages/71/5d/5ca52c0ef86b3d01572131b6709bd531a080995f7e680720e9424328ce1d/shiboken6-6.10.2-cp39-abi3-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/17/f3/f2b63df0251e7cd3172ea28e32ede52739de9566bcefcd0178681538ac81/shiboken6-6.11.1-cp310-abi3-macosx_13_0_universal2.whl name: shiboken6 - version: 6.10.2 - sha256: 10f3c8c5e1b8bee779346f21c10dbc14cff068f0b0b4e62420c82a6bf36ac2e7 - requires_python: '>=3.9,<3.15' -- pypi: https://files.pythonhosted.org/packages/fb/38/3912eb08a3b865b5fcdb4bdce8076cacc211986cee587f5cb62e637791af/shiboken6-6.10.2-cp39-abi3-macosx_13_0_universal2.whl + version: 6.11.1 + sha256: 1a16867f103ef1c662a5f09dfed03273a9f81688b174555162c58e83650a3f02 + requires_python: '>=3.10,<3.15' +- pypi: https://files.pythonhosted.org/packages/52/b5/3f6fb2ee65b534193fb4ef713dd619dc31dadff5d12c16979a7699ad58be/shiboken6-6.11.1-cp310-abi3-win_amd64.whl name: shiboken6 - version: 6.10.2 - sha256: 3bd4e94e9a3c8c1fa8362fd752d399ef39265d5264e4e37bae61cdaa2a00c8c7 - requires_python: '>=3.9,<3.15' + version: 6.11.1 + sha256: c2c6863aa80ec18c0f82cea3417837b279cdc60024ac17123461dc9042577df7 + requires_python: '>=3.10,<3.15' - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl name: six version: 1.17.0 diff --git a/pixi.toml b/pixi.toml index af00e86..75ecef5 100644 --- a/pixi.toml +++ b/pixi.toml @@ -48,6 +48,7 @@ gsl = '*' # GNU Scientific Library; required for pdffit2. [pypi-dependencies] # == [feature.default.pypi-dependencies] pip = '*' # Native package installer easyshapes_app = { path = ".", editable = true, extras = ['dev'] } +pyside6-addons = ">=6.9.3, <7" # Specific features: Set specific Python versions diff --git a/scripts/gen_qrc.py b/scripts/gen_qrc.py new file mode 100644 index 0000000..363e0d0 --- /dev/null +++ b/scripts/gen_qrc.py @@ -0,0 +1,230 @@ +# SPDX-FileCopyrightText: 2021-2026 EasyPeasy contributors +# SPDX-License-Identifier: BSD-3-Clause + +"""Generate the Qt resource file used by the C++/WebAssembly build. + +A browser has no file system, so every QML file, qmldir, font and image +must be compiled into the binary. This script scans the application +sources and the EasyApplication QML modules and writes easyshapes_app.qrc. + +Each entry gets an ``alias``, because the resource path is what QML sees +at runtime. main.cpp calls ``addImportPath("qrc:/")``, so the aliases +must reproduce the module layout: + + qrc:/main.qml + qrc:/Gui/... <- import Gui + qrc:/Backends/... <- import Backends + qrc:/EasyApplication/Gui/... <- import EasyApplication.Gui.* + +Without aliases the entries would land under their on-disk paths, such as +``:/../../EasyApp/src/...``, and no import would resolve. + +Usage: + python scripts/gen_qrc.py + python scripts/gen_qrc.py --easyapp ../../EasyApp/src + python scripts/gen_qrc.py --fonts referenced +""" + +import argparse +import os +import re +import shutil +import sys +from pathlib import Path +from xml.sax.saxutils import quoteattr + +# Files worth compiling into the binary. Everything else is skipped, since +# every byte here ends up in the download the reviewer waits for. +INCLUDED_SUFFIXES = {'.qml', '.js', '.ttf', '.otf', '.png', '.svg', '.jpg', '.jpeg', '.gif'} +INCLUDED_NAMES = {'qmldir'} + +# Directories skipped wholesale. +EXCLUDED_DIRS = {'__pycache__', '.pixi', '.git'} + +# Skipped unless --include-html is given. The Html directory holds the +# Plotly templates and plotly.js, reachable only through QtWebEngine. +# QtWebEngine is Chromium and has no WebAssembly build, so these files are +# dead weight in a browser target but needed by a desktop build that uses +# the EaCharts.Plotly* or EaComponents.BasicReport components. +WEBENGINE_DIRS = {'Html'} + +# Same reason, but a single file inside a module that is otherwise used. +# BasicReport.qml imports QtWebEngine, so leaving it in the resource makes +# qmlimportscanner request a module that does not exist for WebAssembly. +# Nothing in shapes-app instantiates EaComponents.BasicReport, and the +# qmldir entry for it is harmless while the file is absent. +WEBENGINE_NAMES = {'BasicReport.qml'} + +# Skipped unless --include-charts is given. qmlimportscanner reads the QML +# embedded in the resource and links whatever it imports, so the unused +# EasyApplication.Gui.Charts wrappers would pull in two broken modules: +# QtCharts - defines QAbstractAxis, as does QtGraphs. A static build +# linking both fails with duplicate symbols +# QtWebEngine - required by the Plotly* wrappers, no WebAssembly build +# Nothing in shapes-app imports the module. +CHARTS_DIRS = {'Charts'} + +# Individual files skipped. The scratch QML files are not part of the app. +# RemoteController is an EasyApplication automation helper that imports +# QtMultimedia and QtTest; leaving it in makes qmake link both modules into +# the binary. Nothing in shapes-app instantiates it. +EXCLUDED_NAMES = {'test.qml', 'test2.qml', 'RemoteController.qml'} + +# Platform icon formats. Nothing in the QML references them. +EXCLUDED_SUFFIXES = {'.ico', '.icns'} + + +def is_included(path: Path, include_html: bool, include_charts: bool) -> bool: + """Return True if the file belongs in the resource.""" + if path.name in EXCLUDED_NAMES: + return False + if not include_html and path.name in WEBENGINE_NAMES: + return False + if path.suffix.lower() in EXCLUDED_SUFFIXES: + return False + skipped_dirs = set(EXCLUDED_DIRS) + if not include_html: + skipped_dirs |= WEBENGINE_DIRS + if not include_charts: + skipped_dirs |= CHARTS_DIRS + if any(part in skipped_dirs for part in path.parts): + return False + if include_html and path.suffix.lower() == '.html': + return True + return path.name in INCLUDED_NAMES or path.suffix.lower() in INCLUDED_SUFFIXES + + +def referenced_fonts(easyapp_dir: Path) -> set[str]: + """Return the font file names actually loaded by Style/Fonts.qml. + + Fonts.qml loads them as fontPath('PT_Sans', 'PTSans-Regular.ttf'), so + the second argument of every call is the file name. Roughly a quarter + of the shipped font files are referenced; the rest are unused weights + and italics. + """ + fonts_qml = easyapp_dir / 'EasyApplication' / 'Gui' / 'Style' / 'Fonts.qml' + if not fonts_qml.is_file(): + sys.exit(f'error: cannot read {fonts_qml}') + text = fonts_qml.read_text(encoding='utf-8') + return set(re.findall(r'fontPath\(\s*[\'"][^\'"]+[\'"]\s*,\s*[\'"]([^\'"]+)[\'"]', text)) + + +def collect( + root: Path, + alias_prefix: str, + qrc_dir: Path, + keep_fonts: set[str] | None, + include_html: bool, + include_charts: bool, +) -> list: + """Collect (alias, path) pairs for every included file under root. + + alias - where the file appears inside the binary + path - where the file sits on disk, relative to the qrc file + """ + if not root.is_dir(): + sys.exit(f'error: not a directory: {root}') + + entries = [] + for path in sorted(root.rglob('*')): + if not path.is_file() or not is_included(path, include_html, include_charts): + continue + if keep_fonts is not None and path.suffix.lower() in {'.ttf', '.otf'}: + if path.name not in keep_fonts: + continue + alias = path.relative_to(root).as_posix() + if alias_prefix: + alias = f'{alias_prefix}/{alias}' + # Relative paths keep the qrc portable across machines. They do + # assume the EasyApp repository sits next to shapes-app. + disk = Path(_relative_to(path, qrc_dir)).as_posix() + entries.append((alias, disk)) + return entries + + +def _relative_to(path: Path, start: Path) -> str: + """Relative path from start to path, allowing '..' segments.""" + return os.path.relpath(path, start) + + +def main() -> None: + script_dir = Path(__file__).resolve().parent + repo_dir = script_dir.parent + src_dir = repo_dir / 'src' + + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + '--easyapp', + default='../../EasyApp/src', + help='directory holding the EasyApplication package, relative to ' + 'the generated qrc (default: %(default)s)', + ) + parser.add_argument( + '--output', + default=str(src_dir / 'easyshapes_app.qrc'), + help='qrc file to write (default: %(default)s)', + ) + parser.add_argument( + '--fonts', + choices=['all', 'referenced'], + default='all', + help="'referenced' embeds only the fonts loaded by Fonts.qml, which " + 'cuts several megabytes off the download (default: %(default)s)', + ) + parser.add_argument( + '--include-html', + action='store_true', + help='embed the QtWebEngine assets (Gui/Html: Plotly templates and ' + 'plotly.js). Only useful for a desktop build that instantiates the ' + 'EaCharts.Plotly* or EaComponents.BasicReport components, since ' + 'QtWebEngine has no WebAssembly build', + ) + parser.add_argument( + '--include-charts', + action='store_true', + help='embed the EasyApplication.Gui.Charts module. Breaks a ' + 'WebAssembly link, since its QtCharts wrappers clash with QtGraphs ' + 'over QAbstractAxis, and its Plotly wrappers need QtWebEngine', + ) + args = parser.parse_args() + + qrc_path = Path(args.output).resolve() + qrc_dir = qrc_path.parent + app_dir = (src_dir / 'easyshapes_app').resolve() + easyapp_dir = (qrc_dir / args.easyapp).resolve() + + keep_fonts = referenced_fonts(easyapp_dir) if args.fonts == 'referenced' else None + + # The application's own QML keeps its layout: main.qml, Gui/, Backends/ + entries = collect(app_dir, '', qrc_dir, keep_fonts, args.include_html, args.include_charts) + # EasyApplication is aliased under its module name, so that + # 'import EasyApplication.Gui.Elements' resolves under qrc:/ + entries += collect( + easyapp_dir / 'EasyApplication', + 'EasyApplication', + qrc_dir, + keep_fonts, + args.include_html, + args.include_charts, + ) + + lines = ['', ' '] + for alias, disk in entries: + lines.append(f' {disk}') + lines.append(' ') + lines.append('') + content = '\n'.join(lines) + '\n' + + # Keep a one-off copy of whatever was there before, for comparison. + if qrc_path.is_file(): + backup = qrc_path.with_suffix('.qrc.bak') + if not backup.is_file(): + shutil.copyfile(qrc_path, backup) + print(f'saved previous version to {backup}') + + qrc_path.write_text(content, encoding='utf-8') + print(f'wrote {qrc_path} with {len(entries)} entries') + + +if __name__ == '__main__': + main() diff --git a/src/easyshapes_app.pro b/src/easyshapes_app.pro new file mode 100644 index 0000000..f43ad40 --- /dev/null +++ b/src/easyshapes_app.pro @@ -0,0 +1,49 @@ +TEMPLATE = app + +# Application name. On WebAssembly this also names the output files: +# easyshapes_app.html, easyshapes_app.js, easyshapes_app.wasm +TARGET = easyshapes_app + +CONFIG += c++17 + +# Qt modules linked into the binary. On WebAssembly everything is linked +# statically, so a module missing here is simply absent at runtime: +# quick3d - Gui/Pages/SampleModel/MainArea/BaseMol3dQuick.qml +# graphs - Gui/Pages/Analysis/MainArea/Chart.qml, GraphsView.qml +QT += core quick qml quick3d graphs + +# Opt-in QtWebEngine support, needed by the EaCharts.Plotly* and +# EaComponents.BasicReport components: +# qmake easyshapes_app.pro CONFIG+=webengine +# python scripts/gen_qrc.py --include-html +# QtWebEngine is Chromium. It has no WebAssembly build and never will, and +# on Windows it is built for MSVC only - not for MinGW. +webengine { + wasm { + error('CONFIG+=webengine is not possible on WebAssembly: QtWebEngine has no wasm build') + } + QT += webenginequick + DEFINES += EASYSHAPES_WEBENGINE +} + +SOURCES += \ + easyshapes_app/main.cpp + +# Embeds all QML, qmldir, fonts and images, since the browser has no file +# system. Generated - see scripts/gen_qrc.py. +RESOURCES += easyshapes_app.qrc + +# Location of the EasyApplication QML modules. Defaults to the EasyApp repo +# checked out next to shapes-app. Override on the command line: +# qmake easyshapes_app.pro EASYAPP_DIR=/path/to/EasyApp/src +isEmpty(EASYAPP_DIR): EASYAPP_DIR = $$PWD/../../EasyApp/src + +# Additional import path used to resolve QML modules in Qt Creator's code model +QML_IMPORT_PATH += \ + $$PWD/easyshapes_app \ + $$EASYAPP_DIR + +# Additional import path used to resolve QML modules just for Qt Quick Designer +QML_DESIGNER_IMPORT_PATH += \ + $$PWD/easyshapes_app \ + $$EASYAPP_DIR diff --git a/src/easyshapes_app.qmlproject b/src/easyshapes_app.qmlproject index ff45ed3..1c9f27c 100644 --- a/src/easyshapes_app.qmlproject +++ b/src/easyshapes_app.qmlproject @@ -1,12 +1,13 @@ import QmlProject 1.1 + Project { mainFile: "easyshapes_app/main.qml" // List of module and plugin directories passed to QML runtime importPaths: [ "easyshapes_app", - "../../EasyApp/src", // EasyApp + "../../EasyApp/src", // EasyApplication ] // Include .qml files from specified directory and its subdirectories @@ -14,7 +15,7 @@ Project { directory: "easyshapes_app" } QmlFiles { - directory: "../../src/EasyApp" + directory: "../../EasyApp/src/EasyApplication" } // Include .js files from specified directory and its subdirectories @@ -22,7 +23,7 @@ Project { directory: "easyshapes_app" } JavaScriptFiles { - directory: "../../src/EasyApp" + directory: "../../EasyApp/src/EasyApplication" } // Include Module Definition Files (qmldir), as well as .ts and .qrc @@ -33,9 +34,11 @@ Project { recursive: true } Files { - directory: "../../src/EasyApp" + directory: "../../EasyApp/src/EasyApplication" filter: "qmldir;*.ts;*.qrc;*.html" recursive: true } + + mainUiFile: "easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/SampleModel.qml" } diff --git a/src/easyshapes_app.qrc b/src/easyshapes_app.qrc new file mode 100644 index 0000000..7b2700e --- /dev/null +++ b/src/easyshapes_app.qrc @@ -0,0 +1,251 @@ + + + easyshapes_app/Backends/MockBackend.qml + easyshapes_app/Backends/MockQml/Analysis.qml + easyshapes_app/Backends/MockQml/AnalysisConfig.qml + easyshapes_app/Backends/MockQml/BallStructure.qml + easyshapes_app/Backends/MockQml/BilayerStructure.qml + easyshapes_app/Backends/MockQml/Buffer.qml + easyshapes_app/Backends/MockQml/Components.qml + easyshapes_app/Backends/MockQml/ComponentsFiles.qml + easyshapes_app/Backends/MockQml/EquilibrationOutputs.qml + easyshapes_app/Backends/MockQml/Fractions.qml + easyshapes_app/Backends/MockQml/Ions.qml + easyshapes_app/Backends/MockQml/Lamellae.qml + easyshapes_app/Backends/MockQml/LatticeStructure.qml + easyshapes_app/Backends/MockQml/Layers.qml + easyshapes_app/Backends/MockQml/LibraryAssetsEditor.qml + easyshapes_app/Backends/MockQml/MonolayerStructure.qml + easyshapes_app/Backends/MockQml/Project.qml + easyshapes_app/Backends/MockQml/qmldir + easyshapes_app/Backends/MockQml/Report.qml + easyshapes_app/Backends/MockQml/RingStructure.qml + easyshapes_app/Backends/MockQml/RodStructure.qml + easyshapes_app/Backends/MockQml/SampleModel.qml + easyshapes_app/Backends/MockQml/SmilesGenerator.qml + easyshapes_app/Backends/MockQml/Status.qml + easyshapes_app/Backends/MockQml/StructureFiles.qml + easyshapes_app/Backends/MockQml/VesicleStructure.qml + easyshapes_app/Backends/qmldir + easyshapes_app/Gui/ApplicationWindow.qml + easyshapes_app/Gui/Globals/ApplicationInfo.qml + easyshapes_app/Gui/Globals/BackendWrapper.qml + easyshapes_app/Gui/Globals/qmldir + easyshapes_app/Gui/Globals/References.qml + easyshapes_app/Gui/Pages/Analysis/Layout.qml + easyshapes_app/Gui/Pages/Analysis/MainArea/Chart.qml + easyshapes_app/Gui/Pages/Analysis/MainArea/EngineOutput.qml + easyshapes_app/Gui/Pages/Analysis/MainArea/Scattering.qml + easyshapes_app/Gui/Pages/Analysis/Sidebar/Advanced/Groups/EquilibrationOutputs.qml + easyshapes_app/Gui/Pages/Analysis/Sidebar/Advanced/Layout.qml + easyshapes_app/Gui/Pages/Analysis/Sidebar/Basic/Groups/AnalysisConfig.qml + easyshapes_app/Gui/Pages/Analysis/Sidebar/Basic/Layout.qml + easyshapes_app/Gui/Pages/Analysis/Sidebar/Basic/Popups/AddAnalysisConfigFiles.qml + easyshapes_app/Gui/Pages/Home/Content.qml + easyshapes_app/Gui/Pages/Home/Popups/About.qml + easyshapes_app/Gui/Pages/Project/Layout.qml + easyshapes_app/Gui/Pages/Project/MainArea/Description.qml + easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Groups/Examples.qml + easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Groups/GetStarted.qml + easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Groups/Recent.qml + easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Layout.qml + easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Popups/OpenCifFile.qml + easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Popups/ProjectDescription.qml + easyshapes_app/Gui/Pages/Project/Sidebar/Extra/Groups/Scrolling.qml + easyshapes_app/Gui/Pages/Project/Sidebar/Extra/Layout.qml + easyshapes_app/Gui/Pages/Project/Sidebar/Text/Layout.qml + easyshapes_app/Gui/Pages/Report/Layout.qml + easyshapes_app/Gui/Pages/Report/MainArea/Summary.qml + easyshapes_app/Gui/Pages/Report/Sidebar/Basic/Groups/Export.qml + easyshapes_app/Gui/Pages/Report/Sidebar/Basic/Layout.qml + easyshapes_app/Gui/Pages/Report/Sidebar/Extra/Groups/Empty.qml + easyshapes_app/Gui/Pages/Report/Sidebar/Extra/Layout.qml + easyshapes_app/Gui/Pages/SampleModel/Layout.qml + easyshapes_app/Gui/Pages/SampleModel/MainArea/BaseMol3dQuick.qml + easyshapes_app/Gui/Pages/SampleModel/MainArea/Components.qml + easyshapes_app/Gui/Pages/SampleModel/MainArea/ComponentView.qml + easyshapes_app/Gui/Pages/SampleModel/MainArea/FlatShapeView.qml + easyshapes_app/Gui/Pages/SampleModel/MainArea/GraphsView.qml + easyshapes_app/Gui/Pages/SampleModel/MainArea/Lattice.qml + easyshapes_app/Gui/Pages/SampleModel/MainArea/RingShapeView.qml + easyshapes_app/Gui/Pages/SampleModel/MainArea/RodShape.qml + easyshapes_app/Gui/Pages/SampleModel/MainArea/Shape.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Groups/ComponentsFiles.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Groups/LibraryAssetsFiles.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Groups/SmilesGenerator.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Groups/StructureFiles.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Layout.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Popups/AddComponentFiles.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Popups/LoadLibraryAsset.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Popups/ReplaceStructure.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Components/Fractions.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/BallStructure.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/BilayerStructure.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Buffer.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Components.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Lamellae.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/LatticeStructure.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Layers.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/MonolayerStructure.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/RingStructure.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/RodStructure.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/SampleModel.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/VesicleStructure.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Layout.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/CreateNewComponent.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/LoadExistingBufferComponent.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/LoadExistingComponent.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/LoadExistingIon.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/LoadExistingModel.qml + easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/OpenAssetFile.qml + easyshapes_app/Gui/qmldir + easyshapes_app/Gui/Resources/Images/scattering.jpg + easyshapes_app/Gui/Resources/Images/structure.png + easyshapes_app/Gui/Resources/Logos/App.png + easyshapes_app/Gui/Resources/Logos/App.svg + easyshapes_app/Gui/Resources/Logos/ESS.png + easyshapes_app/Gui/StatusBar.qml + easyshapes_app/main.qml + ../../EasyApp/src/EasyApplication/Gui/Animations/ColorReset.qml + ../../EasyApp/src/EasyApplication/Gui/Animations/qmldir + ../../EasyApp/src/EasyApplication/Gui/Animations/ThemeChange.qml + ../../EasyApp/src/EasyApplication/Gui/Animations/TranslationChange.qml + ../../EasyApp/src/EasyApplication/Gui/Components/AboutDialog.qml + ../../EasyApp/src/EasyApplication/Gui/Components/AppBarCentralTabs.qml + ../../EasyApp/src/EasyApplication/Gui/Components/AppBarLeftButtons.qml + ../../EasyApp/src/EasyApplication/Gui/Components/AppBarRightButtons.qml + ../../EasyApp/src/EasyApplication/Gui/Components/ApplicationWindow.qml + ../../EasyApp/src/EasyApplication/Gui/Components/ContentArea.qml + ../../EasyApp/src/EasyApplication/Gui/Components/ContentPage.qml + ../../EasyApp/src/EasyApplication/Gui/Components/GuideWindow.qml + ../../EasyApp/src/EasyApplication/Gui/Components/GuideWindowContainer.qml + ../../EasyApp/src/EasyApplication/Gui/Components/JsonListModel.qml + ../../EasyApp/src/EasyApplication/Gui/Components/ListView.qml + ../../EasyApp/src/EasyApplication/Gui/Components/ListViewDelegate.qml + ../../EasyApp/src/EasyApplication/Gui/Components/ListViewHeader.qml + ../../EasyApp/src/EasyApplication/Gui/Components/ListViewTextInput.qml + ../../EasyApp/src/EasyApplication/Gui/Components/MainContent.qml + ../../EasyApp/src/EasyApplication/Gui/Components/PreferencesDialog.qml + ../../EasyApp/src/EasyApplication/Gui/Components/ProjectDescriptionDialog.qml + ../../EasyApp/src/EasyApplication/Gui/Components/qmldir + ../../EasyApp/src/EasyApplication/Gui/Components/SideBar.qml + ../../EasyApp/src/EasyApplication/Gui/Components/SideBarColumn.qml + ../../EasyApp/src/EasyApplication/Gui/Components/TableView.qml + ../../EasyApp/src/EasyApplication/Gui/Components/TableViewAdvancedLabel.qml + ../../EasyApp/src/EasyApplication/Gui/Components/TableViewButton.qml + ../../EasyApp/src/EasyApplication/Gui/Components/TableViewCheckBox.qml + ../../EasyApp/src/EasyApplication/Gui/Components/TableViewComboBox.qml + ../../EasyApp/src/EasyApplication/Gui/Components/TableViewDelegate.qml + ../../EasyApp/src/EasyApplication/Gui/Components/TableViewHeader.qml + ../../EasyApp/src/EasyApplication/Gui/Components/TableViewLabel.qml + ../../EasyApp/src/EasyApplication/Gui/Components/TableViewLabelControl.qml + ../../EasyApp/src/EasyApplication/Gui/Components/TableViewParameter.qml + ../../EasyApp/src/EasyApplication/Gui/Components/TableViewTextInput.qml + ../../EasyApp/src/EasyApplication/Gui/Components/TableViewTwoRowsAdvancedLabel.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/AppBarTabButton.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/ApplicationWindow.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/Button.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/CheckBox.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/CheckIndicator.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/ComboBox.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/CursorDelegate.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/Dialog.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/DialogButtonBox.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/GroupBox.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/GroupButton.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/GroupColumn.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/GroupRow.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/Label.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/LinkedImage.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/Menu.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/MenuItem.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/ParamComboBox.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/Parameter.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/ParamTextField.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/qmldir + ../../EasyApp/src/EasyApplication/Gui/Elements/RadioButton.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/RadioIndicator.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/RemotePointer.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/RunningLabel.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/ScrollBar.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/ScrollIndicator.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/SideBarButton.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/Slider.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/SliderHandle.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/SpinBox.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/SplashScreen.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/StatusBar.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/StatusBarItem.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/TabBar.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/TabButton.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/TextArea.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/TextField.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/TextInput.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/ToolButton.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/ToolTip.qml + ../../EasyApp/src/EasyApplication/Gui/Elements/ToolTipShadow.qml + ../../EasyApp/src/EasyApplication/Gui/Globals/qmldir + ../../EasyApp/src/EasyApplication/Gui/Globals/Vars.qml + ../../EasyApp/src/EasyApplication/Gui/Logic/Plotting.js + ../../EasyApp/src/EasyApplication/Gui/Logic/ProjectConfig.js + ../../EasyApp/src/EasyApplication/Gui/Logic/qmldir + ../../EasyApp/src/EasyApplication/Gui/Logic/Translate.js + ../../EasyApp/src/EasyApplication/Gui/Logic/Utils.js + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans/EncodeSans-Black.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans/EncodeSans-Bold.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans/EncodeSans-ExtraBold.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans/EncodeSans-ExtraLight.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans/EncodeSans-Light.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans/EncodeSans-Medium.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans/EncodeSans-Regular.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans/EncodeSans-SemiBold.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans/EncodeSans-Thin.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans_Condensed/EncodeSansCondensed-Black.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans_Condensed/EncodeSansCondensed-Bold.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans_Condensed/EncodeSansCondensed-ExtraBold.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans_Condensed/EncodeSansCondensed-ExtraLight.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans_Condensed/EncodeSansCondensed-Light.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans_Condensed/EncodeSansCondensed-Medium.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans_Condensed/EncodeSansCondensed-Regular.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans_Condensed/EncodeSansCondensed-SemiBold.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans_Condensed/EncodeSansCondensed-Thin.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans_Expanded/EncodeSansExpanded-Black.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans_Expanded/EncodeSansExpanded-Bold.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans_Expanded/EncodeSansExpanded-ExtraBold.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans_Expanded/EncodeSansExpanded-ExtraLight.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans_Expanded/EncodeSansExpanded-Light.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans_Expanded/EncodeSansExpanded-Medium.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans_Expanded/EncodeSansExpanded-Regular.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans_Expanded/EncodeSansExpanded-SemiBold.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Encode_Sans_Expanded/EncodeSansExpanded-Thin.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/FontAwesome/Font Awesome 5 Free-Solid-900.otf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/FontAwesome/Font Awesome 6 Free-Solid-900.otf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/FontAwesome/Font Awesome 7 Free-Solid-900.otf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Nunito/Nunito-Black.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Nunito/Nunito-BlackItalic.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Nunito/Nunito-Bold.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Nunito/Nunito-BoldItalic.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Nunito/Nunito-ExtraBold.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Nunito/Nunito-ExtraBoldItalic.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Nunito/Nunito-ExtraLight.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Nunito/Nunito-ExtraLightItalic.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Nunito/Nunito-Italic.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Nunito/Nunito-Light.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Nunito/Nunito-LightItalic.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Nunito/Nunito-Regular.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Nunito/Nunito-SemiBold.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/Nunito/Nunito-SemiBoldItalic.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/PT_Mono/PTMono-Regular.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/PT_Sans/PTSans-Bold.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/PT_Sans/PTSans-BoldItalic.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/PT_Sans/PTSans-Italic.ttf + ../../EasyApp/src/EasyApplication/Gui/Resources/Fonts/PT_Sans/PTSans-Regular.ttf + ../../EasyApp/src/EasyApplication/Gui/Style/Colors.qml + ../../EasyApp/src/EasyApplication/Gui/Style/Fonts.qml + ../../EasyApp/src/EasyApplication/Gui/Style/qmldir + ../../EasyApp/src/EasyApplication/Gui/Style/Sizes.qml + ../../EasyApp/src/EasyApplication/Gui/Style/Times.qml + ../../EasyApp/src/EasyApplication/Logic/Maintenance/qmldir + ../../EasyApp/src/EasyApplication/Logic/Maintenance/Updater.qml + + diff --git a/src/easyshapes_app/Backends/MockBackend.qml b/src/easyshapes_app/Backends/MockBackend.qml index f911028..22915fd 100644 --- a/src/easyshapes_app/Backends/MockBackend.qml +++ b/src/easyshapes_app/Backends/MockBackend.qml @@ -12,8 +12,56 @@ import Backends.MockQml as MockLogic QtObject { property var project: MockLogic.Project + property var sampleModel: MockLogic.SampleModel + property var components: MockLogic.Components + // Advanced sidebar (Sample Model page) domain singletons. + property var componentsFiles: MockLogic.ComponentsFiles + property var structureFiles: MockLogic.StructureFiles + // Library Assets editor (Advanced sidebar) — draft asset create/load/save. + property var libraryAssets: MockLogic.LibraryAssetsEditor + // SMILES generator (Advanced sidebar) — molecule from a SMILES string. + property var smilesGenerator: MockLogic.SmilesGenerator + // Default fractions set tied to the shared component list. + // Used as the fallback for the Fractions sidebar component when no + // per-row override is set. + property var fractions: MockLogic.Fractions { source: MockLogic.Components.loaded } + // Layers owns one Fractions instance per layer (per-row state) and + // shares the same component source so names stay in sync with the + // global components list. + property var layers: MockLogic.Layers + // Lamellae owns distinct inner/outer Fractions instances per lamella. + property var lamellae: MockLogic.Lamellae + // Ring structure parameters (single record, edited inline). + property var ringStructure: MockLogic.RingStructure + // Ball structure parameters (single record, edited inline). + property var ballStructure: MockLogic.BallStructure + // Vesicle structure parameters (single record, edited inline). + property var vesicleStructure: MockLogic.VesicleStructure + // Rod structure parameters (single record, edited inline). + property var rodStructure: MockLogic.RodStructure + // Bilayer structure parameters (single record, edited inline). + property var bilayerStructure: MockLogic.BilayerStructure + // Monolayer structure parameters (single record, edited inline). + property var monolayerStructure: MockLogic.MonolayerStructure + // Lattice structure parameters (single record, edited inline). + property var latticeStructure: MockLogic.LatticeStructure + // Buffer group: solvent selection + buffer components (salts, buffering + // agents). Ions singleton stays (used by the Components C-ion picker). + property var buffer: MockLogic.Buffer + property var ions: MockLogic.Ions property var analysis: MockLogic.Analysis + // Analysis page — equilibration configuration (config files + force + // field + step range), driving the Equilibrate action. + property var analysisConfig: MockLogic.AnalysisConfig + // Analysis page (Advanced sidebar) — one output directory per + // equilibration step, generated from the configured step range. + property var equilibrationOutputs: MockLogic.EquilibrationOutputs property var status: MockLogic.Status property var report: MockLogic.Report + Component.onCompleted: { + layers.fractionsSource = MockLogic.Components.loaded + lamellae.fractionsSource = MockLogic.Components.loaded + } + } diff --git a/src/easyshapes_app/Backends/MockQml/Analysis.qml b/src/easyshapes_app/Backends/MockQml/Analysis.qml index ebb2579..c45be82 100644 --- a/src/easyshapes_app/Backends/MockQml/Analysis.qml +++ b/src/easyshapes_app/Backends/MockQml/Analysis.qml @@ -20,8 +20,17 @@ QtObject { "ymax": 100.0, } + // True once equilibration has finished, gating the engine-output and + // scattering windows on the Analysis page. + property bool equilibrated: false + signal dataPointsChanged(var points) + function equilibrate() { + console.debug("* Equilibration finished (mock)") + equilibrated = true + } + function generateData() { console.debug(`* Generating ${dataSize} data points...`) const xmin = axesRanges.xmin diff --git a/src/easyshapes_app/Backends/MockQml/AnalysisConfig.qml b/src/easyshapes_app/Backends/MockQml/AnalysisConfig.qml new file mode 100644 index 0000000..eabbd5b --- /dev/null +++ b/src/easyshapes_app/Backends/MockQml/AnalysisConfig.qml @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +pragma Singleton + +import QtQuick + + +// Analysis page — equilibration configuration. +// +// Holds: +// * configFiles — Pattern D ListModel of .mdp paths (delete + edit per row). +// * forceFields — JS array of selectable force-field labels (no edits). +// * forceField — currently chosen entry from forceFields. +// * startStep / stopStep — inclusive integer bounds driving which mdp +// files participate in equilibration (defaults 0..6, matching the +// seeded equil0.mdp..equil6.mdp set). +QtObject { + + readonly property var configFiles: ListModel { + id: configFilesModel + ListElement { path: 'analysis/equil0.mdp' } + ListElement { path: 'analysis/equil1.mdp' } + ListElement { path: 'analysis/equil2.mdp' } + ListElement { path: 'analysis/equil3.mdp' } + ListElement { path: 'analysis/equil4.mdp' } + ListElement { path: 'analysis/equil5.mdp' } + ListElement { path: 'analysis/equil6.mdp' } + } + + readonly property var forceFields: [ + 'CHARMM36', + 'CHARMM27', + 'AMBER99SB-ILDN', + 'AMBER14SB', + 'OPLS-AA/L', + 'GROMOS54a7', + 'MARTINI 3', + 'MARTINI 2.2', + 'GAFF' + ] + + property string forceField: 'CHARMM36' + + property int startStep: 0 + property int stopStep: 6 + + function appendFile(item) { + configFilesModel.append({ path: item.path || '' }) + } + + function appendPath(path) { + configFilesModel.append({ path: path }) + } + + function removeFile(index) { + if (index < 0 || index >= configFilesModel.count) return + configFilesModel.remove(index) + } + + function clearFiles() { configFilesModel.clear() } + + // Mock placeholder for opening the file in an editor. + function editFile(index) { + if (index < 0 || index >= configFilesModel.count) return + console.debug('AnalysisConfig.editFile:', configFilesModel.get(index).path) + } + + function setForceField(value) { + if (forceField === value) return + forceField = value + } + + function setStartStep(value) { + const v = Math.max(0, parseInt(value)) + if (isNaN(v) || startStep === v) return + startStep = v + if (stopStep < startStep) stopStep = startStep + } + + function setStopStep(value) { + const v = Math.max(0, parseInt(value)) + if (isNaN(v) || stopStep === v) return + stopStep = v + if (startStep > stopStep) startStep = stopStep + } +} diff --git a/src/easyshapes_app/Backends/MockQml/BallStructure.qml b/src/easyshapes_app/Backends/MockQml/BallStructure.qml new file mode 100644 index 0000000..8b89830 --- /dev/null +++ b/src/easyshapes_app/Backends/MockQml/BallStructure.qml @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +pragma Singleton + +import QtQuick + + +QtObject { + property bool fxz: false + property bool rev: false + property string fill: 'FIBO' +} diff --git a/src/easyshapes_app/Backends/MockQml/BilayerStructure.qml b/src/easyshapes_app/Backends/MockQml/BilayerStructure.qml new file mode 100644 index 0000000..c4dd28c --- /dev/null +++ b/src/easyshapes_app/Backends/MockQml/BilayerStructure.qml @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +pragma Singleton + +import QtQuick + + +QtObject { + property double zsep: 0.0 + property int nside: 1 + property double dmin: 0.5 +} diff --git a/src/easyshapes_app/Backends/MockQml/Buffer.qml b/src/easyshapes_app/Backends/MockQml/Buffer.qml new file mode 100644 index 0000000..cbe07c6 --- /dev/null +++ b/src/easyshapes_app/Backends/MockQml/Buffer.qml @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +pragma Singleton + +import QtQuick + + +QtObject { + + // Solvent — a single selected value. '(None)' means no solvent. + readonly property var solventOptions: ['(None)', 'TIP3', 'Ethanol'] + property string solvent: '(None)' + + // Loaded buffer components. ListModel so the ListView's ItemSelectionModel + // can drive multi-selection and role-based delegate bindings. + // Roles: name, concentration (in mM). dynamicRoles so an edited + // concentration keeps fractional values — a statically-typed role would + // inherit int from the first (whole-number) catalog value and truncate + // floats. + readonly property var components: ListModel { + id: bufferComponentsModel + dynamicRoles: true + } + + // Catalog of loadable buffer components (salts, buffering agents). Identity + // only — concentration is the user's per-experiment choice, set after load. + // Roles: name, description. + readonly property var available: ListModel { + ListElement { name: 'NaCl'; description: 'Sodium chloride — physiological background salt' } + ListElement { name: 'KCl'; description: 'Potassium chloride' } + ListElement { name: 'Tris'; description: 'Tris(hydroxymethyl)aminomethane buffer' } + ListElement { name: 'HEPES'; description: 'Zwitterionic biological buffer (pH 6.8–8.2)' } + ListElement { name: 'MgCl₂'; description: 'Magnesium chloride' } + ListElement { name: 'CaCl₂'; description: 'Calcium chloride' } + ListElement { name: 'Phosphate'; description: 'Sodium phosphate buffer (PBS)' } + ListElement { name: 'EDTA'; description: 'Chelating agent' } + } + + function appendComponent(item) { + bufferComponentsModel.append({ + name: item.name, + concentration: 0 + }) + } + + function removeComponent(index) { + if (index < 0 || index >= bufferComponentsModel.count) return + bufferComponentsModel.remove(index) + } +} diff --git a/src/easyshapes_app/Backends/MockQml/Components.qml b/src/easyshapes_app/Backends/MockQml/Components.qml new file mode 100644 index 0000000..8cda933 --- /dev/null +++ b/src/easyshapes_app/Backends/MockQml/Components.qml @@ -0,0 +1,237 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +pragma Singleton + +import QtQuick + +QtObject { + + // Working set of components currently loaded into the sample model. + // ListModel (not JS array) so EaComponents.ListView's ItemSelectionModel + // can drive Ctrl/Shift multi-selection, and so role-based delegate + // properties (`required property string name` etc.) bind correctly. + // Roles: name, component_type, c_ion, mint, mext. + // c_ion is a counter-ion chosen per component from the shared ion + // library (Ions.available) — not owned here; this only stores the + // selected name (or '' when unset). + readonly property var loaded: ListModel { + id: loadedComponentsModel + } + + // File paths staged while creating a new component. + // Roles: path. + readonly property var pendingFilePaths: ListModel { + id: pendingFilePathsModel + } + + function appendItem(item) { + loadedComponentsModel.append({ + name: item.name, + component_type: item.component_type, + c_ion: item.c_ion !== undefined ? item.c_ion : '', + mint: item.mint, + mext: item.mext + }) + } + + function removeItem(index) { + if (index < 0 || index >= loadedComponentsModel.count) return + loadedComponentsModel.remove(index) + } + + function clear() { + loadedComponentsModel.clear() + } + + function appendPendingFilePath(path) { + pendingFilePathsModel.append({ path: path }) + } + + function removePendingFilePath(index) { + if (index < 0 || index >= pendingFilePathsModel.count) return + pendingFilePathsModel.remove(index) + } + + function clearPendingFilePaths() { + pendingFilePathsModel.clear() + } + + // Atomistic structure used to depict a component in the Components viewer. + // The mock returns one shared template molecule for every component (index + // ignored); the real backend will return each component's own structure. + // + // The PDB text is embedded here (not read from the file) so the mock needs + // no local-file access in Qt Creator's qml runtime. The real backend will + // read the actual file / use the shapespyer readers. + readonly property string _moleculePdb: `REMARK +ATOM 1 N POPCA 1 -1.628 -1.762 11.597 1.00 0.00 A N +ATOM 2 C12 POPCA 1 -2.376 -3.129 11.317 1.00 0.00 A C +ATOM 3 H12A POPCA 1 -2.294 -3.725 12.214 1.00 0.00 A H +ATOM 4 H12B POPCA 1 -1.904 -3.650 10.498 1.00 0.00 A H +ATOM 5 C13 POPCA 1 -0.222 -2.018 11.969 1.00 0.00 A C +ATOM 6 H13A POPCA 1 -0.225 -2.752 12.761 1.00 0.00 A H +ATOM 7 H13B POPCA 1 0.347 -2.478 11.174 1.00 0.00 A H +ATOM 8 H13C POPCA 1 0.321 -1.190 12.400 1.00 0.00 A H +ATOM 9 C14 POPCA 1 -1.612 -0.790 10.423 1.00 0.00 A C +ATOM 10 H14A POPCA 1 -2.639 -0.829 10.093 1.00 0.00 A H +ATOM 11 H14B POPCA 1 -1.360 0.231 10.669 1.00 0.00 A H +ATOM 12 H14C POPCA 1 -0.912 -1.102 9.662 1.00 0.00 A H +ATOM 13 C15 POPCA 1 -2.242 -1.134 12.850 1.00 0.00 A C +ATOM 14 H15A POPCA 1 -1.837 -0.154 13.056 1.00 0.00 A H +ATOM 15 H15B POPCA 1 -2.091 -1.742 13.730 1.00 0.00 A H +ATOM 16 H15C POPCA 1 -3.302 -0.980 12.715 1.00 0.00 A H +ATOM 17 C11 POPCA 1 -3.887 -2.848 11.003 1.00 0.00 A C +ATOM 18 H11A POPCA 1 -4.232 -2.484 11.995 1.00 0.00 A H +ATOM 19 H11B POPCA 1 -4.385 -3.812 10.768 1.00 0.00 A H +ATOM 20 P POPCA 1 -5.348 -1.753 9.168 1.00 0.00 A P +ATOM 21 O13 POPCA 1 -5.595 -0.383 9.566 1.00 0.00 A O +ATOM 22 O14 POPCA 1 -6.302 -2.795 9.531 1.00 0.00 A O +ATOM 23 O12 POPCA 1 -3.970 -1.995 9.851 1.00 0.00 A O +ATOM 24 O11 POPCA 1 -4.981 -1.795 7.640 1.00 0.00 A O +ATOM 25 C1 POPCA 1 -3.932 -2.520 7.052 1.00 0.00 A C +ATOM 26 HA POPCA 1 -4.285 -2.725 6.018 1.00 0.00 A H +ATOM 27 HB POPCA 1 -3.682 -3.511 7.486 1.00 0.00 A H +ATOM 28 C2 POPCA 1 -2.623 -1.763 6.939 1.00 0.00 A C +ATOM 29 HS POPCA 1 -2.582 -1.208 7.900 1.00 0.00 A H +ATOM 30 O21 POPCA 1 -2.777 -0.772 5.947 1.00 0.00 A O +ATOM 31 C21 POPCA 1 -1.865 0.197 5.973 1.00 0.00 A C +ATOM 32 O22 POPCA 1 -0.973 0.362 6.813 1.00 0.00 A O +ATOM 33 C22 POPCA 1 -2.267 1.169 4.884 1.00 0.00 A C +ATOM 34 H2R POPCA 1 -3.173 1.730 5.200 1.00 0.00 A H +ATOM 35 H2S POPCA 1 -2.425 0.490 4.019 1.00 0.00 A H +ATOM 36 C3 POPCA 1 -1.452 -2.658 6.753 1.00 0.00 A C +ATOM 37 HX POPCA 1 -1.422 -3.386 7.592 1.00 0.00 A H +ATOM 38 HY POPCA 1 -0.496 -2.096 6.829 1.00 0.00 A H +ATOM 39 O31 POPCA 1 -1.540 -3.417 5.530 1.00 0.00 A O +ATOM 40 C31 POPCA 1 -0.769 -3.071 4.532 1.00 0.00 A C +ATOM 41 O32 POPCA 1 0.140 -2.241 4.567 1.00 0.00 A O +ATOM 42 C32 POPCA 1 -1.120 -3.860 3.307 1.00 0.00 A C +ATOM 43 H2X POPCA 1 -2.149 -4.280 3.317 1.00 0.00 A H +ATOM 44 H2Y POPCA 1 -0.534 -4.803 3.256 1.00 0.00 A H +ATOM 45 C23 POPCA 1 -1.275 2.333 4.574 1.00 0.00 A C +ATOM 46 H3R POPCA 1 -0.955 2.762 5.548 1.00 0.00 A H +ATOM 47 H3S POPCA 1 -1.895 3.064 4.013 1.00 0.00 A H +ATOM 48 C24 POPCA 1 -0.124 1.785 3.712 1.00 0.00 A C +ATOM 49 H4R POPCA 1 -0.559 1.116 2.939 1.00 0.00 A H +ATOM 50 H4S POPCA 1 0.599 1.205 4.325 1.00 0.00 A H +ATOM 51 C25 POPCA 1 0.703 2.846 3.019 1.00 0.00 A C +ATOM 52 H5R POPCA 1 0.961 3.668 3.721 1.00 0.00 A H +ATOM 53 H5S POPCA 1 0.098 3.364 2.245 1.00 0.00 A H +ATOM 54 C26 POPCA 1 2.011 2.311 2.397 1.00 0.00 A C +ATOM 55 H6R POPCA 1 1.861 1.508 1.645 1.00 0.00 A H +ATOM 56 H6S POPCA 1 2.727 1.959 3.171 1.00 0.00 A H +ATOM 57 C27 POPCA 1 2.556 3.461 1.488 1.00 0.00 A C +ATOM 58 H7R POPCA 1 2.957 4.238 2.174 1.00 0.00 A H +ATOM 59 H7S POPCA 1 1.730 3.855 0.859 1.00 0.00 A H +ATOM 60 C28 POPCA 1 3.645 2.891 0.549 1.00 0.00 A C +ATOM 61 H8R POPCA 1 3.113 2.216 -0.156 1.00 0.00 A H +ATOM 62 H8S POPCA 1 4.359 2.309 1.169 1.00 0.00 A H +ATOM 63 C29 POPCA 1 4.225 4.177 -0.063 1.00 0.00 A C +ATOM 64 H91 POPCA 1 4.684 4.845 0.680 1.00 0.00 A H +ATOM 65 C210 POPCA 1 4.285 4.458 -1.342 1.00 0.00 A C +ATOM 66 H101 POPCA 1 4.674 5.455 -1.597 1.00 0.00 A H +ATOM 67 C211 POPCA 1 3.750 3.728 -2.495 1.00 0.00 A C +ATOM 68 H11R POPCA 1 2.722 4.144 -2.563 1.00 0.00 A H +ATOM 69 H11S POPCA 1 3.712 2.629 -2.337 1.00 0.00 A H +ATOM 70 C212 POPCA 1 4.281 4.069 -3.940 1.00 0.00 A C +ATOM 71 H12R POPCA 1 4.220 5.160 -4.141 1.00 0.00 A H +ATOM 72 H12S POPCA 1 3.663 3.642 -4.758 1.00 0.00 A H +ATOM 73 C213 POPCA 1 5.721 3.534 -4.003 1.00 0.00 A C +ATOM 74 H13R POPCA 1 5.766 2.477 -3.665 1.00 0.00 A H +ATOM 75 H13S POPCA 1 6.405 4.132 -3.363 1.00 0.00 A H +ATOM 76 C214 POPCA 1 6.377 3.519 -5.402 1.00 0.00 A C +ATOM 77 H14R POPCA 1 6.042 2.618 -5.957 1.00 0.00 A H +ATOM 78 H14S POPCA 1 7.451 3.467 -5.120 1.00 0.00 A H +ATOM 79 C215 POPCA 1 6.143 4.755 -6.334 1.00 0.00 A C +ATOM 80 H15R POPCA 1 6.438 5.637 -5.726 1.00 0.00 A H +ATOM 81 H15S POPCA 1 5.058 4.771 -6.571 1.00 0.00 A H +ATOM 82 C216 POPCA 1 6.921 4.780 -7.565 1.00 0.00 A C +ATOM 83 H16R POPCA 1 7.960 4.556 -7.241 1.00 0.00 A H +ATOM 84 H16S POPCA 1 6.924 5.793 -8.021 1.00 0.00 A H +ATOM 85 C217 POPCA 1 6.396 3.829 -8.671 1.00 0.00 A C +ATOM 86 H17R POPCA 1 5.405 4.090 -9.100 1.00 0.00 A H +ATOM 87 H17S POPCA 1 6.183 2.845 -8.201 1.00 0.00 A H +ATOM 88 C218 POPCA 1 7.419 3.731 -9.870 1.00 0.00 A C +ATOM 89 H18R POPCA 1 7.080 2.929 -10.560 1.00 0.00 A H +ATOM 90 H18S POPCA 1 8.445 3.522 -9.496 1.00 0.00 A H +ATOM 91 H18T POPCA 1 7.406 4.715 -10.385 1.00 0.00 A H +ATOM 92 C33 POPCA 1 -0.830 -3.000 2.040 1.00 0.00 A C +ATOM 93 H3X POPCA 1 0.278 -2.920 2.035 1.00 0.00 A H +ATOM 94 H3Y POPCA 1 -1.222 -1.972 2.193 1.00 0.00 A H +ATOM 95 C34 POPCA 1 -1.345 -3.608 0.766 1.00 0.00 A C +ATOM 96 H4X POPCA 1 -2.431 -3.840 0.797 1.00 0.00 A H +ATOM 97 H4Y POPCA 1 -0.831 -4.573 0.570 1.00 0.00 A H +ATOM 98 C35 POPCA 1 -1.032 -2.798 -0.575 1.00 0.00 A C +ATOM 99 H5X POPCA 1 0.045 -2.530 -0.533 1.00 0.00 A H +ATOM 100 H5Y POPCA 1 -1.525 -1.802 -0.574 1.00 0.00 A H +ATOM 101 C36 POPCA 1 -1.505 -3.426 -1.911 1.00 0.00 A C +ATOM 102 H6X POPCA 1 -2.602 -3.530 -1.765 1.00 0.00 A H +ATOM 103 H6Y POPCA 1 -1.091 -4.447 -2.047 1.00 0.00 A H +ATOM 104 C37 POPCA 1 -1.115 -2.553 -3.161 1.00 0.00 A C +ATOM 105 H7X POPCA 1 -0.009 -2.544 -3.264 1.00 0.00 A H +ATOM 106 H7Y POPCA 1 -1.622 -1.566 -3.122 1.00 0.00 A H +ATOM 107 C38 POPCA 1 -1.761 -3.094 -4.443 1.00 0.00 A C +ATOM 108 H8X POPCA 1 -2.855 -3.216 -4.296 1.00 0.00 A H +ATOM 109 H8Y POPCA 1 -1.491 -4.163 -4.579 1.00 0.00 A H +ATOM 110 C39 POPCA 1 -1.349 -2.188 -5.648 1.00 0.00 A C +ATOM 111 H9X POPCA 1 -0.264 -2.213 -5.886 1.00 0.00 A H +ATOM 112 H9Y POPCA 1 -1.654 -1.168 -5.331 1.00 0.00 A H +ATOM 113 C310 POPCA 1 -2.148 -2.523 -6.913 1.00 0.00 A C +ATOM 114 H10X POPCA 1 -3.214 -2.565 -6.604 1.00 0.00 A H +ATOM 115 H10Y POPCA 1 -1.739 -3.469 -7.329 1.00 0.00 A H +ATOM 116 C311 POPCA 1 -1.814 -1.420 -7.906 1.00 0.00 A C +ATOM 117 H11X POPCA 1 -0.721 -1.244 -8.005 1.00 0.00 A H +ATOM 118 H11Y POPCA 1 -2.343 -0.478 -7.646 1.00 0.00 A H +ATOM 119 C312 POPCA 1 -2.433 -1.707 -9.284 1.00 0.00 A C +ATOM 120 H12X POPCA 1 -3.373 -2.283 -9.151 1.00 0.00 A H +ATOM 121 H12Y POPCA 1 -1.656 -2.272 -9.842 1.00 0.00 A H +ATOM 122 C313 POPCA 1 -2.615 -0.439 -10.124 1.00 0.00 A C +ATOM 123 H13X POPCA 1 -1.617 0.048 -10.151 1.00 0.00 A H +ATOM 124 H13Y POPCA 1 -3.499 0.070 -9.684 1.00 0.00 A H +ATOM 125 C314 POPCA 1 -2.874 -0.832 -11.560 1.00 0.00 A C +ATOM 126 H14X POPCA 1 -3.760 -1.477 -11.742 1.00 0.00 A H +ATOM 127 H14Y POPCA 1 -2.004 -1.385 -11.976 1.00 0.00 A H +ATOM 128 C315 POPCA 1 -3.135 0.378 -12.459 1.00 0.00 A C +ATOM 129 H15X POPCA 1 -2.252 1.042 -12.333 1.00 0.00 A H +ATOM 130 H15Y POPCA 1 -4.060 0.876 -12.098 1.00 0.00 A H +ATOM 131 C316 POPCA 1 -3.480 -0.027 -13.887 1.00 0.00 A C +ATOM 132 H16X POPCA 1 -4.277 -0.802 -13.882 1.00 0.00 A H +ATOM 133 H16Y POPCA 1 -2.605 -0.568 -14.306 1.00 0.00 A H +ATOM 134 H16Z POPCA 1 -3.860 0.816 -14.503 1.00 0.00 A H +` + + // Parsed atom list for Qt Quick 3D: [{element, x, y, z}] in Ångström, + // centred on the centroid so the molecule sits around the scene origin. + // Mock-only parser: this template PDB has one clean atom per ATOM line with + // the element as the last whitespace token and x/y/z as tokens 5/6/7. + // The real backend will use the shapespyer readers instead. + function structureAtoms(index) { + const lines = _moleculePdb.split('\n') + const atoms = [] + let cx = 0, cy = 0, cz = 0 + for (let i = 0; i < lines.length; ++i) { + const ln = lines[i] + if (ln.substring(0, 4) !== 'ATOM' && ln.substring(0, 6) !== 'HETATM') + continue + const t = ln.split(/\s+/) + const x = parseFloat(t[5]) + const y = parseFloat(t[6]) + const z = parseFloat(t[7]) + if (isNaN(x) || isNaN(y) || isNaN(z)) + continue + let el = t[t.length - 1] + el = el.charAt(0).toUpperCase() + el.slice(1).toLowerCase() + atoms.push({ element: el, x: x, y: y, z: z }) + cx += x; cy += y; cz += z + } + const n = atoms.length || 1 + cx /= n; cy /= n; cz /= n + for (let j = 0; j < atoms.length; ++j) { + atoms[j].x -= cx; atoms[j].y -= cy; atoms[j].z -= cz + } + return atoms + } + +} diff --git a/src/easyshapes_app/Backends/MockQml/ComponentsFiles.qml b/src/easyshapes_app/Backends/MockQml/ComponentsFiles.qml new file mode 100644 index 0000000..0b3bed0 --- /dev/null +++ b/src/easyshapes_app/Backends/MockQml/ComponentsFiles.qml @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +pragma Singleton + +import QtQuick + + +// Pattern D backend (multi-row list with selection) for the per-component +// file list shown in the Advanced sidebar of the Sample Model page. +// +// The full state is a map of {componentName -> [paths]}. The UI only ever +// sees one component's file list at a time, so we keep that as a single +// ListModel (`files`) and repopulate it whenever the user picks a different +// component through `selectComponent(name)`. Storing the active list as a +// ListModel (rather than a JS array) keeps role-based delegate properties +// and ItemSelectionModel-driven selection working — see QML_MOCKUP_BACKEND +// Pattern D for the rationale. +QtObject { + id: root + + // Name of the component whose files are currently being shown. + // Empty string means no selection — `files` is then empty. + property string selectedComponent: '' + + // Editable name shown in the name field. Tracks `selectedComponent` when a + // component is picked from the dropdown, is emptied by `createNew()`, and is + // the name the file list is persisted under by `save()`. + property string editName: '' + + // Internal map: componentName -> JS array of {path, size} records. + // Mutations go through __syncBack() so the map stays consistent with the + // ListModel. `size` is a fake label — the mock never touches the disk. + property var filesByComponent: ({ + 'DPPC': [ + { path: 'assets/components/DPPC.itp', size: '46 kB' }, + { path: 'assets/components/DPPC.gro', size: '2.4 MB' }, + { path: 'assets/components/DPPC.pdb', size: '3.1 MB' } + ], + 'DOPC': [ + { path: 'assets/components/DOPC.itp', size: '52 kB' }, + { path: 'assets/components/DOPC.gro', size: '2.7 MB' } + ], + 'POPC': [ + { path: 'assets/components/POPC.itp', size: '49 kB' } + ] + }) + + // Fake sizes handed out in order to files the user adds from disk. + readonly property var fakeSizes: ['74 kB', '1.8 MB', '320 kB', '4.2 MB', '12.5 MB'] + property int fakeSizeIndex: 0 + + // Active file list for `selectedComponent`. Roles: path, size. + readonly property var files: ListModel { + id: filesModel + } + + function selectComponent(name) { + selectedComponent = name + editName = name + filesModel.clear() + const list = filesByComponent[name] || [] + for (let i = 0; i < list.length; ++i) { + filesModel.append({ path: list[i].path, size: list[i].size }) + } + } + + // Start a fresh, unsaved component: clear the dropdown selection, the name + // field and the staged file list. + function createNew() { + selectedComponent = '' + editName = '' + filesModel.clear() + } + + function appendFile(path) { + filesModel.append({ path: path, size: nextFakeSize() }) + if (selectedComponent !== '') __syncBack() + } + + function nextFakeSize() { + const label = fakeSizes[fakeSizeIndex % fakeSizes.length] + fakeSizeIndex = fakeSizeIndex + 1 + return label + } + + function removeFile(index) { + if (index < 0 || index >= filesModel.count) return + filesModel.remove(index) + if (selectedComponent !== '') __syncBack() + } + + // Mock placeholder — real backend will open the file in an editor. + function editFile(index) { + if (index < 0 || index >= filesModel.count) return + console.debug('ComponentsFiles.editFile:', filesModel.get(index).path) + } + + // Export a single file of the selected component. Mock placeholder — the + // real backend will copy the file into `destination`. + function exportFile(index, destination) { + if (index < 0 || index >= filesModel.count) return + console.debug('ComponentsFiles.exportFile:', filesModel.get(index).path, + '->', destination) + } + + // Export the whole selected component (all its files). Mock backend + // just logs — the real backend will write into `destination`. + function exportComponent(destination) { + if (selectedComponent === '') return + console.debug('ComponentsFiles.exportComponent:', selectedComponent, + JSON.stringify(filesByComponent[selectedComponent] || []), + '->', destination) + } + + // Persist the current file list to the asset library under `editName`. + // For a freshly created component (`selectedComponent === ''`) this stores + // the staged files under the new name and adopts it as the selection. + // Renaming an existing component leaves the old map entry untouched (mock + // only). Returns the saved name so the caller can mirror it into the + // basic-tab components list. + function save() { + const name = (editName || '').trim() + if (name === '') return '' + const arr = [] + for (let i = 0; i < filesModel.count; ++i) { + const row = filesModel.get(i) + arr.push({ path: row.path, size: row.size }) + } + const copy = Object.assign({}, filesByComponent) + copy[name] = arr + filesByComponent = copy + selectedComponent = name + editName = name + console.debug('ComponentsFiles.save:', name, JSON.stringify(arr)) + return name + } + + function __syncBack() { + if (selectedComponent === '') return + const arr = [] + for (let i = 0; i < filesModel.count; ++i) { + const row = filesModel.get(i) + arr.push({ path: row.path, size: row.size }) + } + // Reassign the map so the `filesByComponent` property emits its + // Changed signal — keeps any future bindings on the map honest. + const copy = Object.assign({}, filesByComponent) + copy[selectedComponent] = arr + filesByComponent = copy + } +} diff --git a/src/easyshapes_app/Backends/MockQml/EquilibrationOutputs.qml b/src/easyshapes_app/Backends/MockQml/EquilibrationOutputs.qml new file mode 100644 index 0000000..af66d84 --- /dev/null +++ b/src/easyshapes_app/Backends/MockQml/EquilibrationOutputs.qml @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +pragma Singleton + +import QtQuick + + +// Analysis page — equilibration step outputs (Advanced sidebar). +// +// Equilibration writes one output directory per step in the configured +// [startStep, stopStep] range of AnalysisConfig, so a 0..6 range yields +// step0/ .. step6/. The GUI picks one directory from `steps` and lists its +// contents from `files`. +// +// Holds: +// * steps — Pattern D ListModel of output directories +// (roles: name, dir, step). +// * files — Pattern D ListModel of the selected directory's contents +// (roles: name, path, size). +// * selectedIndex — row of `steps` whose contents `files` mirrors, -1 when +// nothing is generated or selected. +// +// Both models start empty: no directory exists until Equilibrate runs. +QtObject { + + readonly property var steps: ListModel { id: stepsModel } + + readonly property var files: ListModel { id: filesModel } + + property int selectedIndex: -1 + + readonly property string selectedDir: + selectedIndex >= 0 && selectedIndex < stepsModel.count + ? stepsModel.get(selectedIndex).dir + : '' + + // Files the engine writes into every step directory. Sizes are fake labels + // — the real backend will stat the files on disk. + readonly property var fileTemplate: [ + { suffix: 'tpr', size: '1.4 MB' }, + { suffix: 'gro', size: '3.1 MB' }, + { suffix: 'xtc', size: '18.7 MB' }, + { suffix: 'edr', size: '640 kB' }, + { suffix: 'cpt', size: '2.2 MB' }, + { suffix: 'log', size: '96 kB' } + ] + + // Rebuild the directory set from an inclusive step range. Called after + // equilibration; wipes any previous run and selects the first step. + function generate(start, stop) { + stepsModel.clear() + filesModel.clear() + selectedIndex = -1 + + const from = Math.min(start, stop) + const to = Math.max(start, stop) + for (let s = from; s <= to; ++s) { + stepsModel.append({ + name: qsTr('Step %1').arg(s), + dir: 'analysis/step' + s, + step: s + }) + } + if (stepsModel.count > 0) + selectStep(0) + } + + // Repopulate `files` from the step directory at `index`. + function selectStep(index) { + if (index < 0 || index >= stepsModel.count) { + selectedIndex = -1 + filesModel.clear() + return + } + selectedIndex = index + const entry = stepsModel.get(index) + filesModel.clear() + for (let i = 0; i < fileTemplate.length; ++i) { + const name = 'equil' + entry.step + '.' + fileTemplate[i].suffix + filesModel.append({ + name: name, + path: entry.dir + '/' + name, + size: fileTemplate[i].size + }) + } + } + + // Mock placeholder — real backend will copy the file into `destination`. + function exportFile(index, destination) { + if (index < 0 || index >= filesModel.count) return + console.debug('EquilibrationOutputs.exportFile:', filesModel.get(index).path, + '->', destination) + } + + // Mock placeholder — real backend will copy the selected step directory + // into `destination`. + function exportStep(destination) { + console.debug('EquilibrationOutputs.exportStep:', selectedDir, + 'files=' + filesModel.count, '->', destination) + } + + // Mock placeholder — real backend will reveal the directory in the file + // manager. + function openDir() { + console.debug('EquilibrationOutputs.openDir:', selectedDir) + } +} diff --git a/src/easyshapes_app/Backends/MockQml/Fractions.qml b/src/easyshapes_app/Backends/MockQml/Fractions.qml new file mode 100644 index 0000000..92599eb --- /dev/null +++ b/src/easyshapes_app/Backends/MockQml/Fractions.qml @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick + + +// Fractions backend object — instantiable, NOT a singleton. +// One instance per layer/lamella. Tracks the shared component list via +// `source` and adds per-instance `fracs` / `present` state. Sync between +// `source` and the exposed `model` is owned here so GUI consumers only +// bind `model` and read/write roles via DelegateModel.ReadWrite. +QtObject { + id: root + + // Source list of components this fractions set tracks. + // Typically `MockLogic.Components.loaded`. May be reassigned. + property var source: null + + // ListModel exposed to the GUI ListView. + // Roles: name (mirrored from source), fracs (per-instance), + // present (per-instance). + readonly property var model: ListModel { id: backing } + + Component.onCompleted: rebuild() + onSourceChanged: rebuild() + + // Internal sync. Surgical inserts/removes preserve the per-instance + // fracs/present state of unaffected rows; full rebuild only on reset + // or source reassignment. + property var __sync: Connections { + target: root.source + + function onRowsInserted(parent, first, last) { + for (let i = first; i <= last; ++i) { + const c = root.source.get(i) + backing.insert(i, { name: c.name, fracs: 1, present: true }) + } + } + function onRowsRemoved(parent, first, last) { + for (let i = last; i >= first; --i) backing.remove(i) + } + function onDataChanged(topLeft, bottomRight, roles) { + for (let i = topLeft.row; i <= bottomRight.row; ++i) { + backing.setProperty(i, 'name', root.source.get(i).name) + } + } + function onModelReset() { root.rebuild() } + } + + function rebuild() { + backing.clear() + if (!source) return + for (let i = 0; i < source.count; ++i) { + const c = source.get(i) + backing.append({ name: c.name, fracs: 1, present: true }) + } + } + + function setFracs(index, value) { + if (index < 0 || index >= backing.count) return + backing.setProperty(index, 'fracs', value) + } + + function setPresent(index, value) { + if (index < 0 || index >= backing.count) return + backing.setProperty(index, 'present', value) + } +} diff --git a/src/easyshapes_app/Backends/MockQml/Ions.qml b/src/easyshapes_app/Backends/MockQml/Ions.qml new file mode 100644 index 0000000..19383d1 --- /dev/null +++ b/src/easyshapes_app/Backends/MockQml/Ions.qml @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +pragma Singleton + +import QtQuick + + +QtObject { + + // Loaded ions — at most 2 rows. ListModel so the chip Repeater rebinds + // on append/remove without manual reassignment. Roles: name. + readonly property var loaded: ListModel { + id: loadedIonsModel + } + + // Charge rendered as superscript via markup — EaElements labels + // (AutoText) and the ComboBox (RichText) render it; the markup is part of + // the stored name, so selections round-trip unchanged. + readonly property var available: ListModel { + ListElement { name: 'Na+' } + ListElement { name: 'Cl-' } + ListElement { name: 'Br-' } + ListElement { name: 'K+' } + ListElement { name: 'Ca2+' } + ListElement { name: 'Mg2+' } + ListElement { name: 'Zn2+' } + } + + // Hard cap at the backend layer. The button row also disables itself, + // but mocks must not be the only place enforcing the rule. + readonly property int maxIons: 2 + + function appendItem(item) { + if (loadedIonsModel.count >= maxIons) return + loadedIonsModel.append({ name: item.name }) + } + + function removeItem(index) { + if (index < 0 || index >= loadedIonsModel.count) return + loadedIonsModel.remove(index) + } + + function clear() { + loadedIonsModel.clear() + } + + function saveToCatalog() { + for (let i = 0; i < loadedIonsModel.count; ++i) { + const row = loadedIonsModel.get(i) + if (!row.name) continue + available.append({ name: row.name }) + } + } + + function removeFromCatalog(index) { + if (index < 0 || index >= available.count) return + available.remove(index) + } +} diff --git a/src/easyshapes_app/Backends/MockQml/Lamellae.qml b/src/easyshapes_app/Backends/MockQml/Lamellae.qml new file mode 100644 index 0000000..41ac15d --- /dev/null +++ b/src/easyshapes_app/Backends/MockQml/Lamellae.qml @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +pragma Singleton + +import QtQuick + + +// Pattern D backend (multi-row list with selection) for lamellae, plus +// parallel arrays of per-lamella Fractions instances. Each lamella owns +// distinct inner and outer leaflet fractions so asymmetric lamellae can +// diverge without leaking state between leaflets. +QtObject { + id: root + + // Source of components shared with every leaflet's Fractions instance. + // Each leaflet keeps its own per-instance fracs/present state but + // mirrors the same component name list. + property var fractionsSource: null + + // Table of lamellae. Roles: rmin, innerDmin, outerDmin, shell, symmetric. + readonly property var items: ListModel { + id: lamellaeModel + ListElement { rmin: 0.5; innerDmin: 0.25; outerDmin: 0.3; shell: 1.0; symmetric: true } + } + + // Parallel arrays of Fractions QtObjects, one inner and one outer + // leaflet instance per lamella. + property var innerFractionsInstances: [] + property var outerFractionsInstances: [] + property int itemsRevision: 0 + property int fractionsRevision: 0 + + property var __fractionsComponent: null + + Component.onCompleted: { + __fractionsComponent = Qt.createComponent(Qt.resolvedUrl("Fractions.qml")) + if (__fractionsComponent.status !== Component.Ready) { + console.warn("Lamellae: failed to load Fractions.qml:", __fractionsComponent.errorString()) + return + } + const inner = [] + const outer = [] + for (let i = 0; i < lamellaeModel.count; ++i) { + inner.push(__createFractions()) + outer.push(__createFractions()) + } + innerFractionsInstances = inner + outerFractionsInstances = outer + itemsRevision++ + fractionsRevision++ + } + + function __createFractions() { + // Bind (don't snapshot) so per-instance Fractions follow later + // assignments to root.fractionsSource — singleton init order is + // not deterministic, so the source may be set after this runs. + const f = __fractionsComponent.createObject(root) + f.source = Qt.binding(function() { return root.fractionsSource }) + return f + } + + function appendItem(item) { + lamellaeModel.append({ + rmin: item.rmin, + innerDmin: item.innerDmin, + outerDmin: item.outerDmin, + shell: item.shell, + symmetric: item.symmetric + }) + + const inner = innerFractionsInstances.slice() + const outer = outerFractionsInstances.slice() + inner.push(__createFractions()) + outer.push(__createFractions()) + innerFractionsInstances = inner + outerFractionsInstances = outer + itemsRevision++ + fractionsRevision++ + } + + function removeItem(index) { + if (index < 0 || index >= lamellaeModel.count) return + lamellaeModel.remove(index) + + const inner = innerFractionsInstances.slice() + const outer = outerFractionsInstances.slice() + const removedInner = inner.splice(index, 1)[0] + const removedOuter = outer.splice(index, 1)[0] + if (removedInner) removedInner.destroy() + if (removedOuter) removedOuter.destroy() + innerFractionsInstances = inner + outerFractionsInstances = outer + itemsRevision++ + fractionsRevision++ + } + + function setRmin(index, value) { + if (index < 0 || index >= lamellaeModel.count) return + lamellaeModel.setProperty(index, 'rmin', value) + } + + function setInnerDmin(index, value) { + if (index < 0 || index >= lamellaeModel.count) return + lamellaeModel.setProperty(index, 'innerDmin', value) + } + + function setOuterDmin(index, value) { + if (index < 0 || index >= lamellaeModel.count) return + lamellaeModel.setProperty(index, 'outerDmin', value) + } + + function setShell(index, value) { + if (index < 0 || index >= lamellaeModel.count) return + lamellaeModel.setProperty(index, 'shell', value) + } + + function setSymmetric(index, value) { + if (index < 0 || index >= lamellaeModel.count) return + lamellaeModel.setProperty(index, 'symmetric', value) + itemsRevision++ + } + + function innerFractionsModelAt(index) { + if (index < 0 || index >= innerFractionsInstances.length) return null + const f = innerFractionsInstances[index] + return f ? f.model : null + } + + function outerFractionsModelAt(index) { + if (index < 0 || index >= outerFractionsInstances.length) return null + const f = outerFractionsInstances[index] + return f ? f.model : null + } +} diff --git a/src/easyshapes_app/Backends/MockQml/LatticeStructure.qml b/src/easyshapes_app/Backends/MockQml/LatticeStructure.qml new file mode 100644 index 0000000..c7e2f3d --- /dev/null +++ b/src/easyshapes_app/Backends/MockQml/LatticeStructure.qml @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +pragma Singleton + +import QtQuick + + +QtObject { + property double alpha: 0.0 + property double theta: 0.0 + property double sbuff: 1.0 + property string latticeType: 'LCUB' + property int nlatx: 1 + property int nlaty: 1 + property int nlatz: 1 + + readonly property var latticeTypes: ['LCUB', 'LBCC', 'LFCC', 'LHCP'] +} diff --git a/src/easyshapes_app/Backends/MockQml/Layers.qml b/src/easyshapes_app/Backends/MockQml/Layers.qml new file mode 100644 index 0000000..de3facd --- /dev/null +++ b/src/easyshapes_app/Backends/MockQml/Layers.qml @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +pragma Singleton + +import QtQuick + + +// Pattern D backend (multi-row list with selection) for layers, plus a +// parallel JS array of per-layer Fractions instances. ListElement only +// accepts literal scalars, so the Fractions QtObjects can't live inside +// the ListModel rows — they're maintained in lockstep here. +QtObject { + id: root + + // Source of components shared with every layer's Fractions instance. + // Each layer keeps its own per-instance fracs/present state but + // mirrors the same name list. + property var fractionsSource: null + + // Table of layers. Roles: dmin, rmin. + readonly property var items: ListModel { + id: layersModel + ListElement { dmin: 0.5; rmin: 0.25 } + } + + // Parallel array of Fractions QtObjects, one per layer. + // Bumping the revision token triggers rebinding for `var` consumers, + // since rebuilding the array reference (not its contents) is what + // QML actually signals on. + property var fractionsInstances: [] + property int fractionsRevision: 0 + + property var __fractionsComponent: null + + Component.onCompleted: { + __fractionsComponent = Qt.createComponent(Qt.resolvedUrl("Fractions.qml")) + if (__fractionsComponent.status !== Component.Ready) { + console.warn("Layers: failed to load Fractions.qml:", __fractionsComponent.errorString()) + return + } + const arr = [] + for (let i = 0; i < layersModel.count; ++i) arr.push(__createFractions()) + fractionsInstances = arr + fractionsRevision++ + } + + function __createFractions() { + // Bind (don't snapshot) so per-instance Fractions follow later + // assignments to root.fractionsSource — singleton init order is + // not deterministic, so the source may be set after this runs. + const f = __fractionsComponent.createObject(root) + f.source = Qt.binding(function() { return root.fractionsSource }) + return f + } + + function appendItem(item) { + layersModel.append({ dmin: item.dmin, rmin: item.rmin }) + const arr = fractionsInstances.slice() + arr.push(__createFractions()) + fractionsInstances = arr + fractionsRevision++ + } + + function removeItem(index) { + if (index < 0 || index >= layersModel.count) return + layersModel.remove(index) + const arr = fractionsInstances.slice() + const removed = arr.splice(index, 1)[0] + if (removed) removed.destroy() + fractionsInstances = arr + fractionsRevision++ + } + + function setDmin(index, value) { + if (index < 0 || index >= layersModel.count) return + layersModel.setProperty(index, 'dmin', value) + } + + function setRmin(index, value) { + if (index < 0 || index >= layersModel.count) return + layersModel.setProperty(index, 'rmin', value) + } + + function fractionsModelAt(index) { + if (index < 0 || index >= fractionsInstances.length) return null + const f = fractionsInstances[index] + return f ? f.model : null + } +} diff --git a/src/easyshapes_app/Backends/MockQml/LibraryAssetsEditor.qml b/src/easyshapes_app/Backends/MockQml/LibraryAssetsEditor.qml new file mode 100644 index 0000000..d7211c0 --- /dev/null +++ b/src/easyshapes_app/Backends/MockQml/LibraryAssetsEditor.qml @@ -0,0 +1,213 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +pragma Singleton + +import QtQuick + + +// Backend for the "Library assets" editor (Advanced sidebar). Holds a single +// draft the user either creates from scratch or loads from the library, edits, +// and saves back. Most types are file-bearing assets (Lipid/Surfactant/ +// Component/Ion/Solvent). 'Salt' is the odd one out: it is NOT an asset but a +// named collection of ion assets with stoichiometric counts, so for that type +// the file list is replaced by a two-ion composition. +// +// Instantiation of the typed asset is deferred to save() — while editing we +// keep plain draft fields, so switching the type dropdown never leaves a +// half-built typed object. +QtObject { + id: root + + // Editor mode: + // 'create' — new draft; type dropdown editable. This is the default, so + // the editor opens ready to compose a new asset. + // 'edit' — loaded/saved; type locked (storage is keyed on type). + // 'empty' — reserved; not entered by default. + property string mode: 'create' + + // Type options shown in the dropdown. 'Component (Other)' maps to the + // Component class; 'Salt' is a collection, not a class. + readonly property var typeOptions: ['Lipid', 'Surfactant', 'Component (Other)', 'Ion', 'Solvent', 'Salt'] + + // Draft fields. assetType is a display label from typeOptions. The + // component-only fields (cIon/mint/mext) stay at their defaults for the + // Ion/Solvent/Salt types, where the UI hides them. Defaults to a molecule + // so the editor opens on a typical new-asset form. + property string assetType: 'Lipid' + property string assetName: '' + property string cIon: '' + property int mint: 0 + property int mext: 0 + + // Draft file list (composition of a file-bearing asset). Roles: path, size. + // `size` is a fake label — the mock never touches the disk; files the user + // adds get one from `fakeSizes` in order. The real backend stats the file. + readonly property var paths: ListModel { id: pathsModel } + + readonly property var fakeSizes: ['58 kB', '2.3 MB', '640 kB', '5.1 MB', '11.2 MB'] + property int fakeSizeIndex: 0 + + // Draft salt composition (used only when assetType === 'Salt'). Two fixed + // ion slots. Roles: ion (library ion name, or '' when unset), count (>=1). + readonly property var saltComposition: ListModel { + id: saltCompositionModel + ListElement { ion: ''; count: 1 } + ListElement { ion: ''; count: 1 } + } + + // True when a Salt draft can be saved: a name plus both ion slots filled. + property bool saltReady: false + + // Mock asset library browsed by the Load-from-lib popup. A mix of kinds so + // the browser shows more than components. Roles uniform across rows so the + // statically-typed ListModel keeps every column: file-bearing assets leave + // the ion0/ion1 slots blank; salts leave c_ion/mint/mext at defaults. + // Saving upserts here by (type, name). + readonly property var library: ListModel { + id: libraryModel + ListElement { name: 'DPPC'; type: 'Lipid'; c_ion: ''; mint: 0; mext: 130; ion0: ''; count0: 0; ion1: ''; count1: 0 } + ListElement { name: 'DOPC'; type: 'Lipid'; c_ion: ''; mint: 0; mext: 138; ion0: ''; count0: 0; ion1: ''; count1: 0 } + ListElement { name: 'POPC'; type: 'Lipid'; c_ion: ''; mint: 0; mext: 134; ion0: ''; count0: 0; ion1: ''; count1: 0 } + ListElement { name: 'Cholesterol'; type: 'Lipid'; c_ion: ''; mint: 0; mext: 74; ion0: ''; count0: 0; ion1: ''; count1: 0 } + ListElement { name: 'SDS'; type: 'Surfactant'; c_ion: 'Na+'; mint: 0; mext: 42; ion0: ''; count0: 0; ion1: ''; count1: 0 } + ListElement { name: 'CTAB'; type: 'Surfactant'; c_ion: 'Br-'; mint: 0; mext: 62; ion0: ''; count0: 0; ion1: ''; count1: 0 } + ListElement { name: 'Triton-X100'; type: 'Surfactant'; c_ion: ''; mint: 0; mext: 85; ion0: ''; count0: 0; ion1: ''; count1: 0 } + ListElement { name: 'D2O-buffer'; type: 'Component (Other)'; c_ion: ''; mint: 0; mext: 3; ion0: ''; count0: 0; ion1: ''; count1: 0 } + ListElement { name: 'Na'; type: 'Ion'; c_ion: ''; mint: 0; mext: 0; ion0: ''; count0: 0; ion1: ''; count1: 0 } + ListElement { name: 'Cl'; type: 'Ion'; c_ion: ''; mint: 0; mext: 0; ion0: ''; count0: 0; ion1: ''; count1: 0 } + ListElement { name: 'TIP3'; type: 'Solvent'; c_ion: ''; mint: 0; mext: 0; ion0: ''; count0: 0; ion1: ''; count1: 0 } + ListElement { name: 'Ethanol'; type: 'Solvent'; c_ion: ''; mint: 0; mext: 0; ion0: ''; count0: 0; ion1: ''; count1: 0 } + ListElement { name: 'NaCl'; type: 'Salt'; c_ion: ''; mint: 0; mext: 0; ion0: 'Na+'; count0: 1; ion1: 'Cl-'; count1: 1 } + ListElement { name: 'CaCl2'; type: 'Salt'; c_ion: ''; mint: 0; mext: 0; ion0: 'Ca2+'; count0: 1; ion1: 'Cl-'; count1: 2 } + } + + onAssetNameChanged: _recomputeSalt() + + // Start a fresh, unsaved draft. Type is editable; default to a molecule so + // the component fields are visible to begin with. + function createNew() { + mode = 'create' + assetType = 'Lipid' + assetName = '' + cIon = '' + mint = 0 + mext = 0 + pathsModel.clear() + _clearSalt() + } + + // Adopt a library record into the draft for editing. Type is locked. + function loadAsset(item) { + mode = 'edit' + assetType = item.type + assetName = item.name + cIon = item.c_ion !== undefined ? item.c_ion : '' + mint = item.mint !== undefined ? item.mint : 0 + mext = item.mext !== undefined ? item.mext : 0 + pathsModel.clear() + if (item.type === 'Salt') { + saltCompositionModel.setProperty(0, 'ion', item.ion0) + saltCompositionModel.setProperty(0, 'count', item.count0) + saltCompositionModel.setProperty(1, 'ion', item.ion1) + saltCompositionModel.setProperty(1, 'count', item.count1) + } else { + _clearSalt() + // Mock: stand-in files for the loaded asset, with fake sizes. + pathsModel.append({ path: 'assets/' + item.name + '.itp', size: '47 kB' }) + pathsModel.append({ path: 'assets/' + item.name + '.gro', size: '2.5 MB' }) + } + _recomputeSalt() + } + + function appendPath(path) { + pathsModel.append({ path: path, size: nextFakeSize() }) + } + + function nextFakeSize() { + const label = fakeSizes[fakeSizeIndex % fakeSizes.length] + fakeSizeIndex = fakeSizeIndex + 1 + return label + } + + function removePath(index) { + if (index < 0 || index >= pathsModel.count) return + pathsModel.remove(index) + } + + function editPath(index) { + if (index < 0 || index >= pathsModel.count) return + console.debug('LibraryAssetsEditor.editPath:', pathsModel.get(index).path) + } + + // Export a single file of the draft asset. Mock placeholder — the real + // backend will copy the file into `destination`. + function exportPath(index, destination) { + if (index < 0 || index >= pathsModel.count) return + console.debug('LibraryAssetsEditor.exportPath:', pathsModel.get(index).path, + '->', destination) + } + + function setSaltIon(index, value) { + if (index < 0 || index >= saltCompositionModel.count) return + saltCompositionModel.setProperty(index, 'ion', value) + _recomputeSalt() + } + + function setSaltCount(index, value) { + if (index < 0 || index >= saltCompositionModel.count) return + saltCompositionModel.setProperty(index, 'count', value) + } + + // Instantiate (conceptually) the typed asset / salt from the draft and + // persist it to the library. Mock upserts by (type, name) and flips to edit + // mode so the type locks. Returns false if the draft is incomplete. + function save() { + const name = (assetName || '').trim() + if (name === '') return false + let record + if (assetType === 'Salt') { + if (!saltReady) return false + const r0 = saltCompositionModel.get(0) + const r1 = saltCompositionModel.get(1) + record = { name: name, type: assetType, c_ion: '', mint: 0, mext: 0, + ion0: r0.ion, count0: r0.count, ion1: r1.ion, count1: r1.count } + } else { + if (pathsModel.count === 0) return false + record = { name: name, type: assetType, c_ion: cIon, mint: mint, mext: mext, + ion0: '', count0: 0, ion1: '', count1: 0 } + } + for (let i = 0; i < libraryModel.count; ++i) { + const r = libraryModel.get(i) + if (r.type === assetType && r.name === name) { + libraryModel.set(i, record) + mode = 'edit' + console.debug('LibraryAssetsEditor.save (overwrite):', assetType, name) + return true + } + } + libraryModel.append(record) + mode = 'edit' + console.debug('LibraryAssetsEditor.save (new):', assetType, name) + return true + } + + function exportAsset(destination) { + console.debug('LibraryAssetsEditor.exportAsset:', assetType, assetName, + '->', destination) + } + + function _clearSalt() { + saltCompositionModel.setProperty(0, 'ion', '') + saltCompositionModel.setProperty(0, 'count', 1) + saltCompositionModel.setProperty(1, 'ion', '') + saltCompositionModel.setProperty(1, 'count', 1) + } + + function _recomputeSalt() { + const r0 = saltCompositionModel.get(0) + const r1 = saltCompositionModel.get(1) + saltReady = (assetName || '').trim() !== '' && !!r0.ion && !!r1.ion + } +} diff --git a/src/easyshapes_app/Backends/MockQml/MonolayerStructure.qml b/src/easyshapes_app/Backends/MockQml/MonolayerStructure.qml new file mode 100644 index 0000000..c4dd28c --- /dev/null +++ b/src/easyshapes_app/Backends/MockQml/MonolayerStructure.qml @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +pragma Singleton + +import QtQuick + + +QtObject { + property double zsep: 0.0 + property int nside: 1 + property double dmin: 0.5 +} diff --git a/src/easyshapes_app/Backends/MockQml/RingStructure.qml b/src/easyshapes_app/Backends/MockQml/RingStructure.qml new file mode 100644 index 0000000..1ec3975 --- /dev/null +++ b/src/easyshapes_app/Backends/MockQml/RingStructure.qml @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +pragma Singleton + +import QtQuick + + +QtObject { + property double dmin: 0.5 + property double rmin: 0.25 + property double alpha: 0.0 + property double theta: 0.0 + property double fxz: 0.0 + property double rev: 0.0 +} diff --git a/src/easyshapes_app/Backends/MockQml/RodStructure.qml b/src/easyshapes_app/Backends/MockQml/RodStructure.qml new file mode 100644 index 0000000..5c0770f --- /dev/null +++ b/src/easyshapes_app/Backends/MockQml/RodStructure.qml @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +pragma Singleton + +import QtQuick + + +QtObject { + property double dmin: 0.5 + property double rmin: 0.25 + property int turns: 1 + property bool fxz: false + property bool rev: false +} diff --git a/src/easyshapes_app/Backends/MockQml/SampleModel.qml b/src/easyshapes_app/Backends/MockQml/SampleModel.qml new file mode 100644 index 0000000..95669e4 --- /dev/null +++ b/src/easyshapes_app/Backends/MockQml/SampleModel.qml @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +pragma Singleton + +import QtQuick + +QtObject { + + property bool created: true + + // Loaded sample model — JS array of length 0 or 1. + // Capacity is one by convention (single record), but the value is + // exposed as an array so QML ListView can consume it directly + // (Qt 6 ListView only supports int / ListModel / QAbstractItemModel / JS array). + // Element shape: { name, structure_type, type, description }. + // Preloaded with an empty default model (no name, Ring shape, Discrete + // type) so the user always starts from an editable scaffold. + property var loaded: [{ + name: '', + structure_type: 'Ring', + type: 'Discrete', + description: '' + }] + + // Geometric shape family (the "Shape" column). Lattice is no longer a + // shape — it moved to `type` below. + readonly property var structureTypes: [ + 'Ring', 'Ball', 'Vesicle', 'Rod', 'Bilayer', 'Monolayer' + ] + + // Arrangement of the shape (the "Type" column). 'Lattice' drives the + // Lattice Parameters group's visibility (decoupled from the shape). + readonly property var modelTypes: ['Discrete', 'Lattice'] + + // Externally-observed structure type. Decoupled from `loaded` because + // updateField mutates `loaded[0]` in place (preserving the row delegate + // and any focused TextInput) and that mutation does NOT fire loadedChanged. + // The wrapper writes this property whenever structure_type is updated; + // setLoaded/clear keep it in sync with the active record. + property string currentStructureType: 'Ring' + + // Externally-observed model type (Discrete/Lattice). Mirrored for the + // same reason as currentStructureType — drives Lattice Parameters + // visibility in Layout.qml. + property string currentType: 'Discrete' + + // Catalog of saveable/loadable models (asset library). + // ListModel mirrors the Python QAbstractListModel with roles + // (name, structure_type, description) so delegate code is identical + // for both real and mock backends, and ItemSelectionModel works. + readonly property var availableModels: ListModel { + ListElement { name: 'Samle1_aluv'; structure_type: 'Vesicle'; type: 'Discrete'; description: 'In order to avoid a prolonged pro-inflammatory neutrophil response, signaling downstream of an agonist-activated G protein-coupled receptor (GPCR) has to be rapidly terminated. Among the family of GPCR kinases (GRKs) that regulate receptor phosphorylation and signaling termination, GRK2, which is highly expressed by immune cells, plays an important role.' } + ListElement { name: 'Sample2_nanodisc'; structure_type: 'Ring'; type: 'Discrete'; description: 'The medium chain fatty acid receptor GPR84 as well as formyl peptide receptor 2 (FPR2)' } + ListElement { name: 'Sample3_cubosome'; structure_type: 'Ball'; type: 'Lattice'; description: 'receptors expressed in neutrophils, play a key role in regulating inflammation. In this study, we investigated the effects of GRK2 inhibitors on neutrophil functions induced by GPR84 and FPR2 agonists.' } + ListElement { name: 'Sample4'; structure_type: 'Ring'; type: 'Discrete'; description: 'GRK2 was shown to be expressed in human neutrophils and analysis of subcellular fractions' } + ListElement { name: 'Sample5'; structure_type: 'Ball'; type: 'Discrete'; description: 'revealed a cytosolic localization. The GRK2 inhibitors enhanced and prolonged neutrophil production ' } + ListElement { name: 'Sample6'; structure_type: 'Vesicle'; type: 'Discrete'; description: 'production of reactive oxygen species (ROS) induced by GPR84- but not FPR2-agonists' } + ListElement { name: 'Sample7'; structure_type: 'Rod'; type: 'Discrete'; description: 'suggesting a receptor selective function of GRK2. This suggestion was supported by β-arrestin recruitment data. The ROS production induced by a non β-arrestin recruiting GPR84' } + ListElement { name: 'Sample8'; structure_type: 'Bilayer'; type: 'Discrete'; description: 'This suggestion was supported by β-arrestin recruitment data. The ROS production induced by a non β-arrestin recruiting GPR84 agonist was not affected by the GRK2 inhibitor.' } + ListElement { name: 'Sample9'; structure_type: 'Monolayer'; type: 'Discrete'; description: 'Termination of this β-arrestin independent response relied, similar to the response induced by FPR2 agonists, primarily on the actin cytoskeleton.' } + ListElement { name: 'Samplewithareallylongname'; structure_type: 'Rod'; type: 'Lattice'; description: 'In summary, we show that GPR84 utilizes GRK2 in concert with β-arrestin and actin cytoskeleton dependent processes to fine-tune the activity of the ROS generating NADPH-oxidase in neutrophils.' } + } + + function setLoaded(model) { + console.debug(`Loading sample model '${model.name}'`) + loaded = [{ + name: model.name, + structure_type: model.structure_type, + type: model.type !== undefined ? model.type : 'Discrete', + description: model.description + }] + currentStructureType = loaded[0].structure_type + currentType = loaded[0].type + created = true + } + + function updateField(field, value) { + if (loaded.length === 0) return + loaded[0][field] = value + } + + function clear() { + loaded = [] + currentStructureType = '' + currentType = '' + created = false + } + + function saveToCatalog() { + if (loaded.length === 0 || !loaded[0].name) return + availableModels.append({ + name: loaded[0].name, + structure_type: loaded[0].structure_type, + type: loaded[0].type, + description: loaded[0].description + }) + console.debug(`Saved sample model '${loaded[0].name}' to catalog`) + } + + function removeFromCatalog(index) { + if (index < 0 || index >= availableModels.count) return + availableModels.remove(index) + } + +} diff --git a/src/easyshapes_app/Backends/MockQml/SmilesGenerator.qml b/src/easyshapes_app/Backends/MockQml/SmilesGenerator.qml new file mode 100644 index 0000000..43b91fb --- /dev/null +++ b/src/easyshapes_app/Backends/MockQml/SmilesGenerator.qml @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +pragma Singleton + +import QtQuick + + +// Backend for the "SMILES generator" (Advanced sidebar). Mirrors the shapespyer +// `smiles` tool: a molecule is described by a chemical formula, a short name and +// a SMILES string (hydrogens are added automatically), and is generated into a +// 3D configuration inside a simulation box. The output is a structure file +// (.gro / .pdb) that can feed a component's files. +// +// A .sml record looks like: C12SO4 SDS CCCCCCCCCCCCOS(=O)(=O)[O-] +// followed by a trailing Box Lx Ly Lz. +QtObject { + id: root + + // Molecule identity. `formula` is the chemical formula used as the record + // key in a .sml file (e.g. C12SO4); `moleculeName` is the short name (SDS). + property string moleculeName: '' + property string formula: '' + + // The SMILES string itself. + property string smiles: '' + + // Simulation box edge lengths in nm. + property real boxX: 1.0 + property real boxY: 1.0 + property real boxZ: 1.0 + + // Output configuration format. + readonly property var formatOptions: ['GROMACS (.gro)', 'PDB (.pdb)'] + property string format: 'GROMACS (.gro)' + + // Component type assigned to the component created on Generate. Display + // labels; 'Component (Other)' maps to the stored 'Other'. Defaults to the + // generic 'Component (Other)'. + readonly property var componentTypeOptions: ['Lipid', 'Surfactant', 'Component (Other)'] + property string componentType: 'Component (Other)' + + // Generation flags (mirror the shapespyer getMolecule arguments): + // cisDoubleBonds → dbcis = ['C'] (kink C=C double bonds to cis). + // alignZ → align the backbone to the Z axis. + // flatXZ → flatten into the XZ plane; only acts when alignZ is on. + property bool cisDoubleBonds: false + property bool alignZ: false + property bool flatXZ: false + + // True once a name and a SMILES string are provided. + property bool ready: false + + onMoleculeNameChanged: _recompute() + onSmilesChanged: _recompute() + + function generate() { + if (!ready) return false + console.debug('SmilesGenerator.generate:', + 'formula=' + formula, 'name=' + moleculeName, + 'smiles=' + smiles, + 'box=[' + boxX + ',' + boxY + ',' + boxZ + ']', + 'format=' + format) + return true + } + + function _recompute() { + ready = (moleculeName || '').trim() !== '' && (smiles || '').trim() !== '' + } +} diff --git a/src/easyshapes_app/Backends/MockQml/Status.qml b/src/easyshapes_app/Backends/MockQml/Status.qml index 452ca9e..06ecfc2 100644 --- a/src/easyshapes_app/Backends/MockQml/Status.qml +++ b/src/easyshapes_app/Backends/MockQml/Status.qml @@ -9,10 +9,6 @@ import QtQuick QtObject { readonly property string project: 'Undefined' - readonly property string phasesCount: '1' - readonly property string experimentsCount: '1' - readonly property string calculator: 'CrysPy' - readonly property string minimizer: 'Lmfit (leastsq)' - readonly property string variables: '31 (3 free, 28 fixed)' + readonly property string engine: 'GROMACS' } diff --git a/src/easyshapes_app/Backends/MockQml/StructureFiles.qml b/src/easyshapes_app/Backends/MockQml/StructureFiles.qml new file mode 100644 index 0000000..bc0cf7e --- /dev/null +++ b/src/easyshapes_app/Backends/MockQml/StructureFiles.qml @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +pragma Singleton + +import QtQuick + + +// Pattern D backend (multi-row list with selection) for structure files +// shown in the Advanced sidebar of the Sample Model page. +// Roles: path, size. +// +// `size` is a fake label — the mock never touches the disk. Files the user +// adds get one from `fakeSizes`, handed out in order. The real backend stats +// the file instead. +QtObject { + + readonly property var files: ListModel { + id: filesModel + ListElement { path: 'structure/topology.top'; size: '118 kB' } + ListElement { path: 'structure/coordinates.gro'; size: '2.6 MB' } + ListElement { path: 'structure/serialized.dat'; size: '874 kB' } + } + + readonly property var fakeSizes: ['1.2 MB', '96 kB', '3.4 MB', '512 kB', '17.8 MB'] + property int fakeSizeIndex: 0 + + function appendItem(item) { + appendPath(item.path || '') + } + + function appendPath(path) { + filesModel.append({ path: path, size: nextFakeSize() }) + } + + function nextFakeSize() { + const label = fakeSizes[fakeSizeIndex % fakeSizes.length] + fakeSizeIndex = fakeSizeIndex + 1 + return label + } + + function removeItem(index) { + if (index < 0 || index >= filesModel.count) return + filesModel.remove(index) + } + + function clear() { + filesModel.clear() + } + + // Mock placeholder — real backend will persist to the asset library. + function saveToLib() { + console.debug('StructureFiles.saveToLib: paths=' + filesModel.count) + } + + // Mock placeholder — real backend will write the files into `destination`. + function exportFiles(destination) { + console.debug('StructureFiles.exportFiles: paths=' + filesModel.count, + '->', destination) + } +} diff --git a/src/easyshapes_app/Backends/MockQml/VesicleStructure.qml b/src/easyshapes_app/Backends/MockQml/VesicleStructure.qml new file mode 100644 index 0000000..8b89830 --- /dev/null +++ b/src/easyshapes_app/Backends/MockQml/VesicleStructure.qml @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +pragma Singleton + +import QtQuick + + +QtObject { + property bool fxz: false + property bool rev: false + property string fill: 'FIBO' +} diff --git a/src/easyshapes_app/Backends/MockQml/qmldir b/src/easyshapes_app/Backends/MockQml/qmldir index 7ee148e..25ee99d 100644 --- a/src/easyshapes_app/Backends/MockQml/qmldir +++ b/src/easyshapes_app/Backends/MockQml/qmldir @@ -1,6 +1,26 @@ module MockQml singleton Project Project.qml +singleton SampleModel SampleModel.qml +singleton Components Components.qml +singleton ComponentsFiles ComponentsFiles.qml +singleton StructureFiles StructureFiles.qml +singleton LibraryAssetsEditor LibraryAssetsEditor.qml +singleton SmilesGenerator SmilesGenerator.qml +Fractions Fractions.qml +singleton Layers Layers.qml +singleton Lamellae Lamellae.qml +singleton RingStructure RingStructure.qml +singleton BallStructure BallStructure.qml +singleton VesicleStructure VesicleStructure.qml +singleton RodStructure RodStructure.qml +singleton BilayerStructure BilayerStructure.qml +singleton MonolayerStructure MonolayerStructure.qml +singleton LatticeStructure LatticeStructure.qml +singleton Buffer Buffer.qml +singleton Ions Ions.qml singleton Analysis Analysis.qml +singleton AnalysisConfig AnalysisConfig.qml +singleton EquilibrationOutputs EquilibrationOutputs.qml singleton Report Report.qml singleton Status Status.qml diff --git a/src/easyshapes_app/Backends/real_py/analysis.py b/src/easyshapes_app/Backends/real_py/analysis.py index f66d7c1..ee66b80 100644 --- a/src/easyshapes_app/Backends/real_py/analysis.py +++ b/src/easyshapes_app/Backends/real_py/analysis.py @@ -17,6 +17,7 @@ class Analysis(QObject): dataSizeChanged = Signal() dataPointsChanged = Signal('QVariantList') # Emitted with list axesRangesChanged = Signal() # Emitted when range dict updates + equilibratedChanged = Signal() # Emitted when the equilibration state flips def __init__(self): super().__init__() @@ -28,6 +29,7 @@ def __init__(self): 'ymin': 0.0, 'ymax': 100.0, } + self._equilibrated = False # ------------------------------------------------------------------ # QML-accessible Properties @@ -55,10 +57,24 @@ def axesRanges(self): """ return self._axesRanges + @Property(bool, notify=equilibratedChanged) + def equilibrated(self): + """True once equilibration has finished, so the engine output and + scattering data are available. Placeholder flag for the real MD run.""" + return self._equilibrated + # ------------------------------------------------------------------ # Public Slot Called from QML # ------------------------------------------------------------------ + @Slot() + def equilibrate(self): + """Mark equilibration as finished (placeholder for the real engine run).""" + if self._equilibrated: + return + self._equilibrated = True + self.equilibratedChanged.emit() + @Slot() def generateData(self): """Generate new synthetic data and notify QML.""" diff --git a/src/easyshapes_app/Backends/real_py/status.py b/src/easyshapes_app/Backends/real_py/status.py index f202f51..4aadfbd 100644 --- a/src/easyshapes_app/Backends/real_py/status.py +++ b/src/easyshapes_app/Backends/real_py/status.py @@ -7,20 +7,12 @@ class Status(QObject): projectChanged = Signal() - phasesCountChanged = Signal() - experimentsCountChanged = Signal() - calculatorChanged = Signal() - minimizerChanged = Signal() - variablesChanged = Signal() + engineChanged = Signal() def __init__(self): super().__init__() self._project = 'Undefined' - self._phasesCount = '1' - self._experimentsCount = '1' - self._calculator = 'CrysPy' - self._minimizer = 'Lmfit (leastsq)' - self._variables = '31 (3 free, 28 fixed)' + self._engine = 'GROMACS' ########################## # GUI accessible variables @@ -37,57 +29,13 @@ def project(self, new_value): self._project = new_value self.projectChanged.emit() - @Property(str, notify=phasesCountChanged) - def phasesCount(self): - return self._phasesCount + @Property(str, notify=engineChanged) + def engine(self): + return self._engine - @phasesCount.setter - def phasesCount(self, new_value): - if self._phasesCount == new_value: + @engine.setter + def engine(self, new_value): + if self._engine == new_value: return - self._phasesCount = new_value - self.phasesCountChanged.emit() - - @Property(str, notify=experimentsCountChanged) - def experimentsCount(self): - return self._experimentsCount - - @experimentsCount.setter - def experimentsCount(self, new_value): - if self._experimentsCount == new_value: - return - self._experimentsCount = new_value - self.experimentsCountChanged.emit() - - @Property(str, notify=calculatorChanged) - def calculator(self): - return self._calculator - - @calculator.setter - def calculator(self, new_value): - if self._calculator == new_value: - return - self._calculator = new_value - self.calculatorChanged.emit() - - @Property(str, notify=minimizerChanged) - def minimizer(self): - return self._minimizer - - @minimizer.setter - def minimizer(self, new_value): - if self._minimizer == new_value: - return - self._minimizer = new_value - self.minimizerChanged.emit() - - @Property(str, notify=variablesChanged) - def variables(self): - return self._variables - - @variables.setter - def variables(self, new_value): - if self._variables == new_value: - return - self._variables = new_value - self.variablesChanged.emit() + self._engine = new_value + self.engineChanged.emit() diff --git a/src/easyshapes_app/Gui/ApplicationWindow.qml b/src/easyshapes_app/Gui/ApplicationWindow.qml index 3996326..d991fa7 100644 --- a/src/easyshapes_app/Gui/ApplicationWindow.qml +++ b/src/easyshapes_app/Gui/ApplicationWindow.qml @@ -5,15 +5,17 @@ import QtQuick import QtQuick.Controls -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents +import EasyApplication.Gui.Globals as EaGlobals +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents import Gui as Gui import Gui.Globals as Globals EaComponents.ApplicationWindow { + title: Globals.ApplicationInfo.about.name + /////////////////// // APPLICATION BAR /////////////////// @@ -75,9 +77,9 @@ EaComponents.ApplicationWindow { EaElements.AppBarTabButton { id: sampleModelButton enabled: false - fontIcon: 'puzzle-piece' - text: qsTr('Sample model') - ToolTip.text: qsTr('Sample model definition page') + fontIcon: 'vial' // 'layer-group' 'vials' 'vial' + text: qsTr('Sample Model') // qsTr('Model') + ToolTip.text: qsTr('Sample Model definition page') Component.onCompleted: { Globals.References.applicationWindow.appBarCentralTabs.sampleModelButton = sampleModelButton } @@ -108,16 +110,8 @@ EaComponents.ApplicationWindow { Component.onCompleted: { Globals.References.applicationWindow.appBarCentralTabs.summaryButton = summaryButton } - }, - // Summary page - - // Toolbox page - EaElements.AppBarTabButton { - fontIcon: 'toolbox' - text: qsTr('Toolbox') - ToolTip.text: qsTr('Toolbox with common widgets') } - // Toolbox page + // Summary page ] @@ -131,8 +125,7 @@ EaComponents.ApplicationWindow { Loader { source: 'Pages/Project/Layout.qml' }, Loader { source: 'Pages/SampleModel/Layout.qml' }, Loader { source: 'Pages/Analysis/Layout.qml' }, - Loader { source: 'Pages/Report/Layout.qml' }, - Loader { source: 'Pages/Toolbox/Layout.qml' } + Loader { source: 'Pages/Report/Layout.qml' } ] ///////////// diff --git a/src/easyshapes_app/Gui/Globals/BackendWrapper.qml b/src/easyshapes_app/Gui/Globals/BackendWrapper.qml index e8529b3..e3e7782 100644 --- a/src/easyshapes_app/Gui/Globals/BackendWrapper.qml +++ b/src/easyshapes_app/Gui/Globals/BackendWrapper.qml @@ -34,11 +34,7 @@ QtObject { ///////////// readonly property string statusProject: activeBackend.status.project - readonly property string statusPhasesCount: activeBackend.status.phasesCount - readonly property string statusExperimentsCount: activeBackend.status.experimentsCount - readonly property string statusCalculator: activeBackend.status.calculator - readonly property string statusMinimizer: activeBackend.status.minimizer - readonly property string statusVariables: activeBackend.status.variables + readonly property string statusEngine: activeBackend.status.engine /////////////// // Project page @@ -56,6 +52,258 @@ QtObject { function projectSave() { activeBackend.project.save() } function projectEditInfo(path, new_value) { activeBackend.project.editInfo(path, new_value) } + //////////////////// + // Sample Model page + //////////////////// + + readonly property var sampleModelLoaded: activeBackend.sampleModel.loaded + readonly property var sampleModelAvailable: activeBackend.sampleModel.availableModels + readonly property var sampleModelStructureTypes: activeBackend.sampleModel.structureTypes + readonly property var sampleModelTypes: activeBackend.sampleModel.modelTypes + readonly property string sampleModelCurrentStructureType: activeBackend.sampleModel.currentStructureType + readonly property string sampleModelCurrentType: activeBackend.sampleModel.currentType + + property bool sampleModelCreated: activeBackend.sampleModel.created + onSampleModelCreatedChanged: activeBackend.sampleModel.created = sampleModelCreated + + function sampleModelSetLoaded(model) { activeBackend.sampleModel.setLoaded(model) } + function sampleModelUpdateField(field, value) { + activeBackend.sampleModel.updateField(field, value) + // updateField mutates `loaded[0]` in place to avoid recreating the + // ListView delegate (which would steal focus from any field being + // edited). The trade-off is that `loadedChanged` doesn't fire, so + // bindings derived from `loaded` won't re-evaluate. Mirror the + // structure_type into its own property here so visibility bindings + // (Layers/Lamellae GroupBoxes) update. + if (field === 'structure_type') activeBackend.sampleModel.currentStructureType = value + // Same in-place-mutation caveat for the Discrete/Lattice type: mirror + // it so the Lattice Parameters GroupBox visibility binding updates. + if (field === 'type') activeBackend.sampleModel.currentType = value + } + function sampleModelClear() { activeBackend.sampleModel.clear() } + function sampleModelSaveToCatalog() { activeBackend.sampleModel.saveToCatalog() } + function sampleModelRemoveFromCatalog(index) { activeBackend.sampleModel.removeFromCatalog(index) } + + // Components group (Sample Model sidebar) + readonly property var componentsLoaded: activeBackend.components.loaded + readonly property var componentsPendingFilePaths: activeBackend.components.pendingFilePaths + + function componentsAppend(item) { activeBackend.components.appendItem(item) } + function componentsRemove(index) { activeBackend.components.removeItem(index) } + function componentsClear() { activeBackend.components.clear() } + function componentsAppendPendingFilePath(path) { activeBackend.components.appendPendingFilePath(path) } + function componentsRemovePendingFilePath(index) { activeBackend.components.removePendingFilePath(index) } + function componentsClearPendingFilePaths() { activeBackend.components.clearPendingFilePaths() } + + // Atomistic structure of a loaded component, for the Components viewer: + // parsed [{element, x, y, z}] (Å) fed to the Qt Quick 3D viewer. The mock + // returns one shared template molecule for every component. + function componentStructureAtoms(index) { return activeBackend.components.structureAtoms(index) } + + // Global default Fractions set (fallback for the Fractions sidebar + // component). Layers/Lamellae do NOT use this — see the per-row + // fraction accessors below. + readonly property var fractionsModel: activeBackend.fractions.model + + function fractionsSetFracs(index, value) { activeBackend.fractions.setFracs(index, value) } + function fractionsSetPresent(index, value) { activeBackend.fractions.setPresent(index, value) } + + // Layers (Sample Model sidebar). One Fractions instance per layer is + // owned by the backend; the GUI binds the fractions list of the + // currently-selected layer via layersFractionsModelAt(row). + readonly property var layersItems: activeBackend.layers.items + readonly property int layersFractionsRevision: activeBackend.layers.fractionsRevision + + function layersAppend(item) { activeBackend.layers.appendItem(item) } + function layersRemove(index) { activeBackend.layers.removeItem(index) } + function layersSetDmin(index, value) { activeBackend.layers.setDmin(index, value) } + function layersSetRmin(index, value) { activeBackend.layers.setRmin(index, value) } + // Callers binding through this must also reference layersFractionsRevision + // in their binding body to re-evaluate when rows are inserted/removed. + function layersFractionsModelAt(index) { return activeBackend.layers.fractionsModelAt(index) } + + // Lamellae (Sample Model sidebar). One inner and one outer Fractions + // instance are owned per lamella; asymmetric lamellae bind both models. + readonly property var lamellaeItems: activeBackend.lamellae.items + readonly property int lamellaeItemsRevision: activeBackend.lamellae.itemsRevision + readonly property int lamellaeFractionsRevision: activeBackend.lamellae.fractionsRevision + + function lamellaeAppend(item) { activeBackend.lamellae.appendItem(item) } + function lamellaeRemove(index) { activeBackend.lamellae.removeItem(index) } + function lamellaeSetRmin(index, value) { activeBackend.lamellae.setRmin(index, value) } + function lamellaeSetInnerDmin(index, value) { activeBackend.lamellae.setInnerDmin(index, value) } + function lamellaeSetOuterDmin(index, value) { activeBackend.lamellae.setOuterDmin(index, value) } + function lamellaeSetShell(index, value) { activeBackend.lamellae.setShell(index, value) } + function lamellaeSetSymmetric(index, value) { activeBackend.lamellae.setSymmetric(index, value) } + // Callers binding through these must also reference lamellaeFractionsRevision + // in their binding body to re-evaluate when rows are inserted/removed. + function lamellaeInnerFractionsModelAt(index) { return activeBackend.lamellae.innerFractionsModelAt(index) } + function lamellaeOuterFractionsModelAt(index) { return activeBackend.lamellae.outerFractionsModelAt(index) } + + // Ring structure (Sample Model sidebar). Single-record fielded form + // shown only when sampleModelCurrentStructureType === 'Ring'. + readonly property var ringStructure: activeBackend.ringStructure + + // Ball structure (Sample Model sidebar). Single-record fielded form + // shown only when sampleModelCurrentStructureType === 'Ball'. + readonly property var ballStructure: activeBackend.ballStructure + + // Vesicle structure (Sample Model sidebar). Single-record fielded form + // shown only when sampleModelCurrentStructureType === 'Vesicle'. + readonly property var vesicleStructure: activeBackend.vesicleStructure + + // Rod structure (Sample Model sidebar). Single-record fielded form + // shown only when sampleModelCurrentStructureType === 'Rod'. + readonly property var rodStructure: activeBackend.rodStructure + + // Bilayer structure (Sample Model sidebar). Single-record fielded form + // shown only when sampleModelCurrentStructureType === 'Bilayer'. + readonly property var bilayerStructure: activeBackend.bilayerStructure + + // Monolayer structure (Sample Model sidebar). Single-record fielded form + // shown only when sampleModelCurrentStructureType === 'Monolayer'. + readonly property var monolayerStructure: activeBackend.monolayerStructure + + // Lattice parameters (Sample Model sidebar). Single-record fielded form + // shown only when sampleModelCurrentType === 'Lattice' (the Discrete/Lattice + // arrangement, decoupled from the shape). + readonly property var latticeStructure: activeBackend.latticeStructure + + // Buffer group (Sample Model sidebar). Solvent is a single selected value + // ('(None)' / TIP3 / Ethanol); buffer components are a row list (salts, + // buffering agents) backed by a shared catalog. + readonly property var bufferSolventOptions: activeBackend.buffer.solventOptions + property string bufferSolvent: activeBackend.buffer.solvent + onBufferSolventChanged: activeBackend.buffer.solvent = bufferSolvent + + readonly property var bufferComponents: activeBackend.buffer.components + readonly property var bufferComponentsAvailable: activeBackend.buffer.available + + function bufferComponentsAppend(item) { activeBackend.buffer.appendComponent(item) } + function bufferComponentsRemove(index) { activeBackend.buffer.removeComponent(index) } + + readonly property var ionsLoaded: activeBackend.ions.loaded + readonly property var ionsAvailable: activeBackend.ions.available + + function ionsAppend(item) { activeBackend.ions.appendItem(item) } + function ionsRemove(index) { activeBackend.ions.removeItem(index) } + function ionsClear() { activeBackend.ions.clear() } + function ionsSaveToCatalog() { activeBackend.ions.saveToCatalog() } + function ionsRemoveFromCatalog(index) { activeBackend.ions.removeFromCatalog(index) } + + // Components Files (Sample Model Advanced sidebar). Per-component file + // list driven by `componentsFilesSelectedComponent`; switching the + // selection repopulates `componentsFilesFiles`. + readonly property var componentsFilesFiles: activeBackend.componentsFiles.files + readonly property string componentsFilesSelectedComponent: activeBackend.componentsFiles.selectedComponent + readonly property string componentsFilesEditName: activeBackend.componentsFiles.editName + + function componentsFilesSelect(name) { activeBackend.componentsFiles.selectComponent(name) } + function componentsFilesCreateNew() { activeBackend.componentsFiles.createNew() } + function componentsFilesSetEditName(name) { activeBackend.componentsFiles.editName = name } + function componentsFilesAppend(path) { activeBackend.componentsFiles.appendFile(path) } + function componentsFilesRemove(index) { activeBackend.componentsFiles.removeFile(index) } + function componentsFilesEditFile(index) { activeBackend.componentsFiles.editFile(index) } + function componentsFilesExportFile(index, destination) { activeBackend.componentsFiles.exportFile(index, destination) } + function componentsFilesExportComponent(destination) { activeBackend.componentsFiles.exportComponent(destination) } + + // Save the current file list to the asset library and load the component + // into the basic-tab components list (under whatever name is in the name + // field) if it isn't already there. + function componentsFilesSave() { + const name = activeBackend.componentsFiles.save() + if (!name) + return + const loaded = activeBackend.components.loaded + for (let i = 0; i < loaded.count; ++i) { + if (loaded.get(i).name === name) + return + } + activeBackend.components.appendItem({ + name: name, + component_type: 'Other', + c_ion: '', + mint: 0, + mext: 0 + }) + } + + // Structure Files (Sample Model Advanced sidebar). List of structure- + // related files plus actions for saving the set to the asset library + // and re-seeding structure parameters from a serialized data file. + readonly property var structureFilesFiles: activeBackend.structureFiles.files + + function structureFilesAppend(item) { activeBackend.structureFiles.appendItem(item) } + function structureFilesAppendPath(path) { activeBackend.structureFiles.appendPath(path) } + function structureFilesRemove(index) { activeBackend.structureFiles.removeItem(index) } + function structureFilesClear() { activeBackend.structureFiles.clear() } + function structureFilesSaveToLib() { activeBackend.structureFiles.saveToLib() } + function structureFilesExport(destination) { activeBackend.structureFiles.exportFiles(destination) } + + // Library Assets editor (Advanced sidebar). A single draft asset the user + // creates or loads from the library, edits, and saves back. `mode` is + // 'empty' | 'create' | 'edit'; the type is editable only while creating. + readonly property string libraryAssetsMode: activeBackend.libraryAssets.mode + readonly property var libraryAssetsTypeOptions: activeBackend.libraryAssets.typeOptions + readonly property string libraryAssetsType: activeBackend.libraryAssets.assetType + readonly property string libraryAssetsName: activeBackend.libraryAssets.assetName + readonly property string libraryAssetsCIon: activeBackend.libraryAssets.cIon + readonly property int libraryAssetsMint: activeBackend.libraryAssets.mint + readonly property int libraryAssetsMext: activeBackend.libraryAssets.mext + readonly property var libraryAssetsPaths: activeBackend.libraryAssets.paths + readonly property var libraryAssetsLibrary: activeBackend.libraryAssets.library + // Salt composition (used when the type is 'Salt'). `saltReady` is true once + // a name and both ions are set. + readonly property var libraryAssetsSaltComposition: activeBackend.libraryAssets.saltComposition + readonly property bool libraryAssetsSaltReady: activeBackend.libraryAssets.saltReady + + function libraryAssetsCreateNew() { activeBackend.libraryAssets.createNew() } + function libraryAssetsSetSaltIon(index, value) { activeBackend.libraryAssets.setSaltIon(index, value) } + function libraryAssetsSetSaltCount(index, value) { activeBackend.libraryAssets.setSaltCount(index, value) } + function libraryAssetsLoad(item) { activeBackend.libraryAssets.loadAsset(item) } + function libraryAssetsSetType(value) { activeBackend.libraryAssets.assetType = value } + function libraryAssetsSetName(value) { activeBackend.libraryAssets.assetName = value } + function libraryAssetsSetCIon(value) { activeBackend.libraryAssets.cIon = value } + function libraryAssetsSetMint(value) { activeBackend.libraryAssets.mint = value } + function libraryAssetsSetMext(value) { activeBackend.libraryAssets.mext = value } + function libraryAssetsAppendPath(path) { activeBackend.libraryAssets.appendPath(path) } + function libraryAssetsRemovePath(index) { activeBackend.libraryAssets.removePath(index) } + function libraryAssetsEditPath(index) { activeBackend.libraryAssets.editPath(index) } + function libraryAssetsExportPath(index, destination) { activeBackend.libraryAssets.exportPath(index, destination) } + function libraryAssetsSave() { activeBackend.libraryAssets.save() } + function libraryAssetsExport(destination) { activeBackend.libraryAssets.exportAsset(destination) } + + // SMILES generator (Advanced sidebar). Builds a 3D molecular configuration + // from a SMILES string. `smilesReady` is true once a name and SMILES string + // are provided. + readonly property string smilesMoleculeName: activeBackend.smilesGenerator.moleculeName + readonly property string smilesFormula: activeBackend.smilesGenerator.formula + readonly property string smilesString: activeBackend.smilesGenerator.smiles + readonly property real smilesBoxX: activeBackend.smilesGenerator.boxX + readonly property real smilesBoxY: activeBackend.smilesGenerator.boxY + readonly property real smilesBoxZ: activeBackend.smilesGenerator.boxZ + readonly property var smilesFormatOptions: activeBackend.smilesGenerator.formatOptions + readonly property string smilesFormat: activeBackend.smilesGenerator.format + readonly property var smilesComponentTypeOptions: activeBackend.smilesGenerator.componentTypeOptions + readonly property string smilesComponentType: activeBackend.smilesGenerator.componentType + readonly property bool smilesCisDoubleBonds: activeBackend.smilesGenerator.cisDoubleBonds + readonly property bool smilesAlignZ: activeBackend.smilesGenerator.alignZ + readonly property bool smilesFlatXZ: activeBackend.smilesGenerator.flatXZ + readonly property bool smilesReady: activeBackend.smilesGenerator.ready + + function smilesSetMoleculeName(value) { activeBackend.smilesGenerator.moleculeName = value } + function smilesSetFormula(value) { activeBackend.smilesGenerator.formula = value } + function smilesSetString(value) { activeBackend.smilesGenerator.smiles = value } + function smilesSetBoxX(value) { activeBackend.smilesGenerator.boxX = value } + function smilesSetBoxY(value) { activeBackend.smilesGenerator.boxY = value } + function smilesSetBoxZ(value) { activeBackend.smilesGenerator.boxZ = value } + function smilesSetFormat(value) { activeBackend.smilesGenerator.format = value } + function smilesSetComponentType(value) { activeBackend.smilesGenerator.componentType = value } + function smilesSetCisDoubleBonds(value) { activeBackend.smilesGenerator.cisDoubleBonds = value } + function smilesSetAlignZ(value) { activeBackend.smilesGenerator.alignZ = value } + function smilesSetFlatXZ(value) { activeBackend.smilesGenerator.flatXZ = value } + function smilesGenerate() { activeBackend.smilesGenerator.generate() } + //////////////// // Analysis page //////////////// @@ -63,6 +311,43 @@ QtObject { // All the properties and methods related to the analysis page // are defined directly in the Backends/MockQml/Analysis.qml !!! + // Analysis configuration group — equilibration .mdp files, the chosen + // force field, and the inclusive [start, stop] step range. + readonly property var analysisConfigFiles: activeBackend.analysisConfig.configFiles + readonly property var analysisConfigForceFields: activeBackend.analysisConfig.forceFields + readonly property string analysisConfigForceField: activeBackend.analysisConfig.forceField + readonly property int analysisConfigStartStep: activeBackend.analysisConfig.startStep + readonly property int analysisConfigStopStep: activeBackend.analysisConfig.stopStep + + function analysisConfigAppendPath(path) { activeBackend.analysisConfig.appendPath(path) } + function analysisConfigRemoveFile(index) { activeBackend.analysisConfig.removeFile(index) } + function analysisConfigEditFile(index) { activeBackend.analysisConfig.editFile(index) } + function analysisConfigSetForceField(value) { activeBackend.analysisConfig.setForceField(value) } + function analysisConfigSetStartStep(value) { activeBackend.analysisConfig.setStartStep(value) } + function analysisConfigSetStopStep(value) { activeBackend.analysisConfig.setStopStep(value) } + + // Run the engine, then materialise one output directory per step in the + // configured [startStep, stopStep] range for the Advanced sidebar. + function analysisEquilibrate() { + activeBackend.analysis.equilibrate() + activeBackend.equilibrationOutputs.generate( + activeBackend.analysisConfig.startStep, + activeBackend.analysisConfig.stopStep) + } + + // Equilibration outputs group (Analysis Advanced sidebar). `steps` lists + // the per-step output directories; `files` holds the contents of the one + // currently selected. Both are empty until equilibration has run. + readonly property var equilOutputsSteps: activeBackend.equilibrationOutputs.steps + readonly property var equilOutputsFiles: activeBackend.equilibrationOutputs.files + readonly property int equilOutputsSelectedIndex: activeBackend.equilibrationOutputs.selectedIndex + readonly property string equilOutputsSelectedDir: activeBackend.equilibrationOutputs.selectedDir + + function equilOutputsSelect(index) { activeBackend.equilibrationOutputs.selectStep(index) } + function equilOutputsExportFile(index, destination) { activeBackend.equilibrationOutputs.exportFile(index, destination) } + function equilOutputsExportStep(destination) { activeBackend.equilibrationOutputs.exportStep(destination) } + function equilOutputsOpenDir() { activeBackend.equilibrationOutputs.openDir() } + /////////////// // Summary page /////////////// diff --git a/src/easyshapes_app/Gui/Globals/References.qml b/src/easyshapes_app/Gui/Globals/References.qml index 53be81c..c59d207 100644 --- a/src/easyshapes_app/Gui/Globals/References.qml +++ b/src/easyshapes_app/Gui/Globals/References.qml @@ -34,6 +34,21 @@ QtObject { } } }, + 'samplemodel': { + 'sidebar': { + 'basic': { + 'popups': { + 'LoadExistingModel': null, + 'LoadExistingComponent': null, + 'CreateNewComponent': null, + 'OpenAssetFile': null + }, + 'groups': { + 'sampleModel': null + } + } + } + }, 'analysis': { 'sidebar': { 'basic': { diff --git a/src/easyshapes_app/Gui/Pages/Analysis/Layout.qml b/src/easyshapes_app/Gui/Pages/Analysis/Layout.qml index 4c9d546..401fb61 100644 --- a/src/easyshapes_app/Gui/Pages/Analysis/Layout.qml +++ b/src/easyshapes_app/Gui/Pages/Analysis/Layout.qml @@ -5,36 +5,43 @@ import QtQuick import QtQuick.Controls -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Globals as EaGlobals +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents import Gui.Globals as Globals EaComponents.ContentPage { + // Two static main-area windows. Both are always shown; their content stays + // empty until equilibration is finished (gated on the backend's + // `analysis.equilibrated` flag, set by the sidebar's Equilibrate button). mainView: EaComponents.MainContent { tabs: [ - EaElements.TabButton { text: qsTr('Chart') } + EaElements.TabButton { text: qsTr("Engine output") }, + EaElements.TabButton { text: qsTr("Scattering") } ] items: [ - Loader { source: 'MainArea/Chart.qml' } + Loader { source: "MainArea/EngineOutput.qml" }, + Loader { source: "MainArea/Scattering.qml" } ] } sideBar: EaComponents.SideBar { tabs: [ - EaElements.TabButton { text: qsTr('Basic controls') } + EaElements.TabButton { text: qsTr("Basic controls") }, + EaElements.TabButton { text: qsTr("Advanced controls") } ] items: [ - Loader { source: 'Sidebar/Basic/Layout.qml' } + Loader { source: "Sidebar/Basic/Layout.qml" }, + Loader { source: "Sidebar/Advanced/Layout.qml" } ] - continueButton.text: qsTr('Continue') + continueButton.text: qsTr("Continue") continueButton.onClicked: { console.debug(`Clicking '${continueButton.text}' button ::: ${this}`) diff --git a/src/easyshapes_app/Gui/Pages/Analysis/MainArea/Chart.qml b/src/easyshapes_app/Gui/Pages/Analysis/MainArea/Chart.qml index 4f8ce10..2afc6bf 100644 --- a/src/easyshapes_app/Gui/Pages/Analysis/MainArea/Chart.qml +++ b/src/easyshapes_app/Gui/Pages/Analysis/MainArea/Chart.qml @@ -5,8 +5,8 @@ import QtQuick import QtGraphs -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Elements as EaElements +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements import Gui.Globals as Globals @@ -67,7 +67,7 @@ Rectangle { } - titleText: 'x' + titleText: "x" min: Globals.BackendWrapper.activeBackend.analysis.axesRanges["xmin"] max: Globals.BackendWrapper.activeBackend.analysis.axesRanges["xmax"] } @@ -89,7 +89,7 @@ Rectangle { } - titleText: 'y' + titleText: "y" min: Globals.BackendWrapper.activeBackend.analysis.axesRanges["ymin"] max: Globals.BackendWrapper.activeBackend.analysis.axesRanges["ymax"] } diff --git a/src/easyshapes_app/Gui/Pages/Analysis/MainArea/EngineOutput.qml b/src/easyshapes_app/Gui/Pages/Analysis/MainArea/EngineOutput.qml new file mode 100644 index 0000000..3864d3e --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/Analysis/MainArea/EngineOutput.qml @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements + +import Gui.Globals as Globals + + +// Main-area window: combined engine output (GROMACS stdout + stderr; other +// engines later). The tab is always present, but stays empty until equilibration +// is finished. +// DRAFT CONTAINER — real engine streaming not implemented yet. +Rectangle { + id: root + + readonly property bool equilibrated: Globals.BackendWrapper.activeBackend.analysis.equilibrated === true + + color: EaStyle.Colors.mainContentBackground + + // Before equilibration — no content, just a hint. + EaElements.Label { + anchors.centerIn: parent + visible: !root.equilibrated + color: EaStyle.Colors.themeForegroundMinor + text: qsTr("Run Equilibrate to see the engine output.") + } + + // After equilibration — the combined stdout/stderr log (placeholder). + EaElements.TextArea { + anchors.fill: parent + anchors.margins: EaStyle.Sizes.fontPixelSize + visible: root.equilibrated + readOnly: true + font.family: EaStyle.Fonts.monoFontFamily + text: qsTr("Engine stdout / stderr will stream here.\n\n" + + "(Placeholder — GROMACS wiring not implemented yet.)") + } + +} diff --git a/src/easyshapes_app/Gui/Pages/Analysis/MainArea/Scattering.qml b/src/easyshapes_app/Gui/Pages/Analysis/MainArea/Scattering.qml new file mode 100644 index 0000000..cb1a010 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/Analysis/MainArea/Scattering.qml @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements + +import Gui.Globals as Globals + + +// Main-area window: scattering data. The tab is always present, but stays empty +// until equilibration is finished, then shows the scattering image. +// DRAFT CONTAINER — the image is a static mockup asset for now. +// Drop the picture at Gui/Resources/Images/scattering.png to display it here. +Rectangle { + id: root + + readonly property bool equilibrated: Globals.BackendWrapper.activeBackend.analysis.equilibrated === true + + color: EaStyle.Colors.mainContentBackground + clip: true // crop the image overflow from PreserveAspectCrop + + // Before equilibration — no content, just a hint. + EaElements.Label { + anchors.centerIn: parent + visible: !root.equilibrated + color: EaStyle.Colors.themeForegroundMinor + text: qsTr("Run Equilibrate to see the scattering data.") + } + + // After equilibration — the scattering image (mockup asset). PreserveAspectCrop + // scales the image to cover the whole window (matching width or height, + // whichever needs the larger scale, based on the window vs image aspect ratio) + // and crops the overflow — so the window is always fully filled, no bars. + Image { + id: scatteringImage + anchors.fill: parent + visible: root.equilibrated && status === Image.Ready + fillMode: Image.PreserveAspectCrop + source: "../../../Resources/Images/scattering.jpg" + } + + // Fallback while the image asset isn't present yet. + EaElements.Label { + anchors.centerIn: parent + visible: root.equilibrated && scatteringImage.status !== Image.Ready + color: EaStyle.Colors.themeForegroundMinor + text: qsTr("Drop scattering.png into Gui/Resources/Images/ to show it here.") + } + +} diff --git a/src/easyshapes_app/Gui/Pages/Analysis/Sidebar/Advanced/Groups/EquilibrationOutputs.qml b/src/easyshapes_app/Gui/Pages/Analysis/Sidebar/Advanced/Groups/EquilibrationOutputs.qml new file mode 100644 index 0000000..ff85a1c --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/Analysis/Sidebar/Advanced/Groups/EquilibrationOutputs.qml @@ -0,0 +1,152 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents + +import Gui.Globals as Globals + + +EaElements.GroupColumn { + id: root + property double halfWidth: (EaStyle.Sizes.sideBarContentWidth - EaStyle.Sizes.fontPixelSize) / 2 + + // The step directories only exist once equilibration has run; before that + // the dropdown and the file list are empty. + readonly property bool hasSteps: Globals.BackendWrapper.equilOutputsSteps + ? Globals.BackendWrapper.equilOutputsSteps.count > 0 + : false + + // Row of the file picked in the list, -1 when nothing is selected. Drives + // whether Export writes one file or the whole step directory. + readonly property int selectedFileRow: stepFilesList.selectedIndexes.length > 0 + ? stepFilesList.selectedIndexes[0].row + : -1 + + // Step selector. Picking a step repopulates the file list below. + Column { + EaElements.Label { + enabled: false + text: qsTr("Step") + } + EaElements.ComboBox { + id: stepSelector + width: root.halfWidth + textRole: "name" + model: Globals.BackendWrapper.equilOutputsSteps + enabled: root.hasSteps + displayText: currentIndex < 0 ? "" : currentText + + onActivated: (i) => Globals.BackendWrapper.equilOutputsSelect(i) + + // Mirror the dropdown index from the backend's selected step so + // regenerating the outputs resets the label too. + function syncIndex() { + currentIndex = Globals.BackendWrapper.equilOutputsSelectedIndex + } + + Component.onCompleted: syncIndex() + Connections { + target: Globals.BackendWrapper.equilOutputsSteps + function onCountChanged() { stepSelector.syncIndex() } + } + Connections { + target: Globals.BackendWrapper + function onEquilOutputsSelectedIndexChanged() { stepSelector.syncIndex() } + } + } + } + + EaComponents.ListView { + id: stepFilesList + defaultInfoText: qsTr("No equilibration outputs — run Equilibrate first") + multiSelection: false + + columnWidths: [ + EaStyle.Sizes.fontPixelSize * 2.5, + -1, + EaStyle.Sizes.fontPixelSize * 5 + ] + + header: EaComponents.ListViewHeader { + EaComponents.TableViewLabel { + text: qsTr("№") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("File") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Size") + color: EaStyle.Colors.themeForegroundMinor + } + } + + model: Globals.BackendWrapper.equilOutputsFiles + + delegateModelAccess: DelegateModel.ReadWrite + + delegate: EaComponents.ListViewDelegate { + required property int index + required property string name + required property string size + + EaComponents.TableViewLabel { + text: index + 1 + enabled: false + } + EaComponents.TableViewLabel { + text: name + elide: Text.ElideLeft + } + EaComponents.TableViewLabel { + text: size + enabled: false + } + } + } + + Grid { + columns: 2 + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.SideBarButton { + fontIcon: "file-export" + text: qsTr("Export") + width: root.halfWidth + ToolTip.text: qsTr("Export the selected file to disk, or the whole step directory when no file is selected") + enabled: Globals.BackendWrapper.equilOutputsSelectedDir !== "" + onClicked: exportDestinationDialog.open() + } + + EaElements.SideBarButton { + fontIcon: "folder-open" + text: qsTr("Open directory") + width: root.halfWidth + ToolTip.text: qsTr("Open the selected step directory in the system file browser") + enabled: Globals.BackendWrapper.equilOutputsSelectedDir !== "" + onClicked: Globals.BackendWrapper.equilOutputsOpenDir() + } + } + + // Destination picker for Export. What gets written is decided here rather + // than on the button, so the selection is read when the user confirms. + FolderDialog { + id: exportDestinationDialog + title: qsTr("Choose a destination directory") + onAccepted: { + const destination = selectedFolder.toString() + if (root.selectedFileRow >= 0) + Globals.BackendWrapper.equilOutputsExportFile(root.selectedFileRow, destination) + else + Globals.BackendWrapper.equilOutputsExportStep(destination) + } + } +} diff --git a/src/easyshapes_app/Gui/Pages/Analysis/Sidebar/Advanced/Layout.qml b/src/easyshapes_app/Gui/Pages/Analysis/Sidebar/Advanced/Layout.qml new file mode 100644 index 0000000..a09a67c --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/Analysis/Sidebar/Advanced/Layout.qml @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls + +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents + + +EaComponents.SideBarColumn { + + EaElements.GroupBox { + title: qsTr("Equilibration outputs") + icon: "folder-open" + collapsed: false + + Loader { source: "Groups/EquilibrationOutputs.qml" } + } + +} diff --git a/src/easyshapes_app/Gui/Pages/Analysis/Sidebar/Basic/Groups/AnalysisConfig.qml b/src/easyshapes_app/Gui/Pages/Analysis/Sidebar/Basic/Groups/AnalysisConfig.qml new file mode 100644 index 0000000..ea5d70e --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/Analysis/Sidebar/Basic/Groups/AnalysisConfig.qml @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents + +import Gui.Globals as Globals + +EaElements.GroupColumn { + id: root + property double halfWidth: (EaStyle.Sizes.sideBarContentWidth - EaStyle.Sizes.fontPixelSize) / 2 + // Two quarters plus one fontPixelSize gap add up to exactly halfWidth, so + // the step pair fills the right half of the ForceField row. + property double quarterWidth: (EaStyle.Sizes.sideBarContentWidth - 3 * EaStyle.Sizes.fontPixelSize) / 4 + + EaComponents.ListView { + id: configFilesList + defaultInfoText: qsTr("No configuration files added") + multiSelection: false + // The seeded set is equil0..equil6, so show all seven without scrolling. + maxRowCountShow: 7 + + columnWidths: [ + EaStyle.Sizes.fontPixelSize * 2.5, + -1, + EaStyle.Sizes.tableRowHeight, + EaStyle.Sizes.tableRowHeight + ] + + header: EaComponents.ListViewHeader { + EaComponents.TableViewLabel { + text: qsTr("№") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Path") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel {} + EaComponents.TableViewLabel {} + } + + model: Globals.BackendWrapper.analysisConfigFiles + + delegateModelAccess: DelegateModel.ReadWrite + + delegate: EaComponents.ListViewDelegate { + required property int index + required property string path + + EaComponents.TableViewLabel { + text: index + 1 + enabled: false + } + EaComponents.TableViewLabel { + text: path + elide: Text.ElideLeft + } + EaComponents.TableViewButton { + fontIcon: "edit" + ToolTip.text: qsTr("Edit this file") + onClicked: Globals.BackendWrapper.analysisConfigEditFile(index) + } + EaComponents.TableViewButton { + fontIcon: "minus-circle" + ToolTip.text: qsTr("Remove this file") + onClicked: Globals.BackendWrapper.analysisConfigRemoveFile(index) + } + } + } + + Grid { + columns: 1 + + EaElements.SideBarButton { + fontIcon: "plus-circle" + text: qsTr("Add file(s)") + width: root.halfWidth + ToolTip.text: qsTr("Pick one or more .mdp files from disk") + onClicked: addFilesLoader.item.open() + } + } + + // ForceField selector on the left, step-range pair on the right. + Row { + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.ComboBox { + width: root.halfWidth + topInset: forceFieldLabel.height + topPadding: topInset + padding + model: Globals.BackendWrapper.analysisConfigForceFields + currentIndex: Math.max( + 0, + Globals.BackendWrapper.analysisConfigForceFields.indexOf( + Globals.BackendWrapper.analysisConfigForceField)) + onActivated: (i) => Globals.BackendWrapper.analysisConfigSetForceField( + Globals.BackendWrapper.analysisConfigForceFields[i]) + + EaElements.Label { + id: forceFieldLabel + text: qsTr("ForceField") + } + } + + Row { + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.Parameter { + width: root.quarterWidth + title: qsTr("Start step") + inputMethodHints: Qt.ImhDigitsOnly + validator: IntValidator { bottom: 0 } + text: Globals.BackendWrapper.analysisConfigStartStep + onEditingFinished: Globals.BackendWrapper.analysisConfigSetStartStep(text) + } + + EaElements.Parameter { + width: root.quarterWidth + title: qsTr("Stop step") + inputMethodHints: Qt.ImhDigitsOnly + validator: IntValidator { bottom: 0 } + text: Globals.BackendWrapper.analysisConfigStopStep + onEditingFinished: Globals.BackendWrapper.analysisConfigSetStopStep(text) + } + } + } + + Loader { + id: addFilesLoader + source: "../Popups/AddAnalysisConfigFiles.qml" + } +} diff --git a/src/easyshapes_app/Gui/Pages/Analysis/Sidebar/Basic/Groups/GetStarted.qml b/src/easyshapes_app/Gui/Pages/Analysis/Sidebar/Basic/Groups/GetStarted.qml deleted file mode 100644 index 7b73f23..0000000 --- a/src/easyshapes_app/Gui/Pages/Analysis/Sidebar/Basic/Groups/GetStarted.qml +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2024 EasyApp contributors -// SPDX-License-Identifier: BSD-3-Clause -// © 2024 Contributors to the EasyApp project - -import QtQuick -import QtQuick.Controls - -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents -import EasyApp.Gui.Logic as EaLogic - -import Gui.Globals as Globals - -EaElements.GroupColumn { - - // 1st row - EaElements.GroupRow { - spacing: EaStyle.Sizes.fontPixelSize - - // button - EaElements.SideBarButton { - id: generateDataButton - - fontIcon: 'plus-circle' - text: qsTr('Generate new data') - - onClicked: { - console.debug(`Clicking '${text}' button ::: ${this}`) - Globals.BackendWrapper.activeBackend.analysis.generateData() - } - } - // button - - // text input - EaElements.ParamTextField { - height: generateDataButton.height - - inputMethodHints: Qt.ImhDigitsOnly - - value: Globals.BackendWrapper.activeBackend.analysis.dataSize - units: 'points' - - onTextChanged: { - Globals.BackendWrapper.activeBackend.analysis.dataSize = text - } - } - // text input - } - // 1st row -} diff --git a/src/easyshapes_app/Gui/Pages/Analysis/Sidebar/Basic/Layout.qml b/src/easyshapes_app/Gui/Pages/Analysis/Sidebar/Basic/Layout.qml index e59ca3c..2c6defe 100644 --- a/src/easyshapes_app/Gui/Pages/Analysis/Sidebar/Basic/Layout.qml +++ b/src/easyshapes_app/Gui/Pages/Analysis/Sidebar/Basic/Layout.qml @@ -5,20 +5,42 @@ import QtQuick import QtQuick.Controls -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents import Gui.Globals as Globals - EaComponents.SideBarColumn { EaElements.GroupBox { - title: qsTr('Get started') - icon: 'rocket' + title: qsTr("Equilibration setup") + icon: "sliders-h" collapsed: false - Loader { source: 'Groups/GetStarted.qml' } + Loader { source: "Groups/AnalysisConfig.qml" } } + // Centered "Equilibrate" call-to-action below the groups. + Item { + width: parent.width + height: equilibrateButton.height + EaStyle.Sizes.fontPixelSize + + EaElements.SideBarButton { + id: equilibrateButton + anchors.centerIn: parent + text: qsTr("Equilibrate") + fontIcon: "magic" + width: EaStyle.Sizes.sideBarContentWidth + enabled: Globals.BackendWrapper.analysisConfigFiles + ? Globals.BackendWrapper.analysisConfigFiles.count > 0 + : false + onClicked: { + console.debug("Equilibrate clicked") + // Runs the engine and generates the per-step output + // directories consumed by the Advanced tab. + Globals.BackendWrapper.analysisEquilibrate() + } + } + } } diff --git a/src/easyshapes_app/Gui/Pages/Analysis/Sidebar/Basic/Popups/AddAnalysisConfigFiles.qml b/src/easyshapes_app/Gui/Pages/Analysis/Sidebar/Basic/Popups/AddAnalysisConfigFiles.qml new file mode 100644 index 0000000..313c43e --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/Analysis/Sidebar/Basic/Popups/AddAnalysisConfigFiles.qml @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Dialogs + +import Gui.Globals as Globals + + +FileDialog { + fileMode: FileDialog.OpenFiles + nameFilters: [ + "GROMACS run parameter files (*.mdp)", + "Any (*)" + ] + + onAccepted: { + for (let i = 0; i < selectedFiles.length; ++i) { + Globals.BackendWrapper.analysisConfigAppendPath(selectedFiles[i].toString()) + } + } +} diff --git a/src/easyshapes_app/Gui/Pages/Home/Content.qml b/src/easyshapes_app/Gui/Pages/Home/Content.qml index e0b7a19..420851d 100644 --- a/src/easyshapes_app/Gui/Pages/Home/Content.qml +++ b/src/easyshapes_app/Gui/Pages/Home/Content.qml @@ -4,9 +4,9 @@ import QtQuick -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Elements as EaElements +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Globals as EaGlobals +import EasyApplication.Gui.Elements as EaElements import Gui.Globals as Globals diff --git a/src/easyshapes_app/Gui/Pages/Home/Popups/About.qml b/src/easyshapes_app/Gui/Pages/Home/Popups/About.qml index 11815fd..c1af059 100644 --- a/src/easyshapes_app/Gui/Pages/Home/Popups/About.qml +++ b/src/easyshapes_app/Gui/Pages/Home/Popups/About.qml @@ -4,8 +4,8 @@ import QtQuick -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Components as EaComponents +import EasyApplication.Gui.Globals as EaGlobals +import EasyApplication.Gui.Components as EaComponents import Gui.Globals as Globals diff --git a/src/easyshapes_app/Gui/Pages/Project/Layout.qml b/src/easyshapes_app/Gui/Pages/Project/Layout.qml index 6ebbb95..f7c6116 100644 --- a/src/easyshapes_app/Gui/Pages/Project/Layout.qml +++ b/src/easyshapes_app/Gui/Pages/Project/Layout.qml @@ -5,10 +5,10 @@ import QtQuick import QtQuick.Controls -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Globals as EaGlobals +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents import Gui.Globals as Globals diff --git a/src/easyshapes_app/Gui/Pages/Project/MainArea/Description.qml b/src/easyshapes_app/Gui/Pages/Project/MainArea/Description.qml index 8ce72b2..eab134f 100644 --- a/src/easyshapes_app/Gui/Pages/Project/MainArea/Description.qml +++ b/src/easyshapes_app/Gui/Pages/Project/MainArea/Description.qml @@ -4,8 +4,8 @@ import QtQuick -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Elements as EaElements +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements import Gui.Globals as Globals diff --git a/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Groups/Examples.qml b/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Groups/Examples.qml index 065031c..bfd3695 100644 --- a/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Groups/Examples.qml +++ b/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Groups/Examples.qml @@ -5,11 +5,11 @@ import QtQuick import QtQuick.Controls -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents -import EasyApp.Gui.Logic as EaLogic +import EasyApplication.Gui.Globals as EaGlobals +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents +import EasyApplication.Gui.Logic as EaLogic import Gui.Globals as Globals diff --git a/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Groups/GetStarted.qml b/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Groups/GetStarted.qml index cb1ab18..83e643d 100644 --- a/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Groups/GetStarted.qml +++ b/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Groups/GetStarted.qml @@ -6,11 +6,11 @@ import QtQuick import QtQuick.Controls //import QtQuick.Dialogs -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents -import EasyApp.Gui.Logic as EaLogic +import EasyApplication.Gui.Globals as EaGlobals +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents +import EasyApplication.Gui.Logic as EaLogic import Gui.Globals as Globals diff --git a/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Groups/Recent.qml b/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Groups/Recent.qml index 8357f95..e9158c1 100644 --- a/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Groups/Recent.qml +++ b/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Groups/Recent.qml @@ -6,11 +6,11 @@ import QtQuick import QtQuick.Controls import QtCore -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents -import EasyApp.Gui.Logic as EaLogic +import EasyApplication.Gui.Globals as EaGlobals +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents +import EasyApplication.Gui.Logic as EaLogic import Gui.Globals as Globals diff --git a/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Layout.qml b/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Layout.qml index 23952b4..0cdbc68 100644 --- a/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Layout.qml +++ b/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Layout.qml @@ -5,8 +5,8 @@ import QtQuick import QtQuick.Controls -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents import Gui.Globals as Globals diff --git a/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Popups/OpenCifFile.qml b/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Popups/OpenCifFile.qml index 649bb8a..29580d6 100644 --- a/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Popups/OpenCifFile.qml +++ b/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Popups/OpenCifFile.qml @@ -6,8 +6,8 @@ import QtQuick import QtQuick.Controls import QtQuick.Dialogs -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Components as EaComponents +import EasyApplication.Gui.Globals as EaGlobals +import EasyApplication.Gui.Components as EaComponents import Gui.Globals as Globals diff --git a/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Popups/ProjectDescription.qml b/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Popups/ProjectDescription.qml index dcc2277..68d21c2 100644 --- a/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Popups/ProjectDescription.qml +++ b/src/easyshapes_app/Gui/Pages/Project/Sidebar/Basic/Popups/ProjectDescription.qml @@ -5,8 +5,8 @@ import QtQuick import QtQuick.Controls -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Components as EaComponents +import EasyApplication.Gui.Globals as EaGlobals +import EasyApplication.Gui.Components as EaComponents import Gui.Globals as Globals diff --git a/src/easyshapes_app/Gui/Pages/Project/Sidebar/Extra/Groups/Scrolling.qml b/src/easyshapes_app/Gui/Pages/Project/Sidebar/Extra/Groups/Scrolling.qml index 4bc8920..5521228 100644 --- a/src/easyshapes_app/Gui/Pages/Project/Sidebar/Extra/Groups/Scrolling.qml +++ b/src/easyshapes_app/Gui/Pages/Project/Sidebar/Extra/Groups/Scrolling.qml @@ -4,8 +4,8 @@ import QtQuick -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Elements as EaElements +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements Column { diff --git a/src/easyshapes_app/Gui/Pages/Project/Sidebar/Extra/Layout.qml b/src/easyshapes_app/Gui/Pages/Project/Sidebar/Extra/Layout.qml index 273d95a..fd00806 100644 --- a/src/easyshapes_app/Gui/Pages/Project/Sidebar/Extra/Layout.qml +++ b/src/easyshapes_app/Gui/Pages/Project/Sidebar/Extra/Layout.qml @@ -5,8 +5,8 @@ import QtQuick import QtQuick.Controls -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents import Gui.Globals as Globals diff --git a/src/easyshapes_app/Gui/Pages/Project/Sidebar/Text/Layout.qml b/src/easyshapes_app/Gui/Pages/Project/Sidebar/Text/Layout.qml index 3c9c9d6..9c4629b 100644 --- a/src/easyshapes_app/Gui/Pages/Project/Sidebar/Text/Layout.qml +++ b/src/easyshapes_app/Gui/Pages/Project/Sidebar/Text/Layout.qml @@ -4,8 +4,8 @@ import QtQuick -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents EaComponents.SideBarColumn {} diff --git a/src/easyshapes_app/Gui/Pages/Report/Layout.qml b/src/easyshapes_app/Gui/Pages/Report/Layout.qml index 3beddbd..6604f52 100644 --- a/src/easyshapes_app/Gui/Pages/Report/Layout.qml +++ b/src/easyshapes_app/Gui/Pages/Report/Layout.qml @@ -5,10 +5,10 @@ import QtQuick import QtQuick.Controls -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Globals as EaGlobals +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents import Gui.Globals as Globals diff --git a/src/easyshapes_app/Gui/Pages/Report/MainArea/Summary.qml b/src/easyshapes_app/Gui/Pages/Report/MainArea/Summary.qml index 0168ba3..8c1bcd4 100644 --- a/src/easyshapes_app/Gui/Pages/Report/MainArea/Summary.qml +++ b/src/easyshapes_app/Gui/Pages/Report/MainArea/Summary.qml @@ -5,9 +5,9 @@ import QtQuick import QtQuick.Controls -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Animations as EaAnimations -import EasyApp.Gui.Elements as EaElements +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Animations as EaAnimations +import EasyApplication.Gui.Elements as EaElements import Gui.Globals as Globals diff --git a/src/easyshapes_app/Gui/Pages/Report/Sidebar/Basic/Groups/Export.qml b/src/easyshapes_app/Gui/Pages/Report/Sidebar/Basic/Groups/Export.qml index e80a6b4..7cc971b 100644 --- a/src/easyshapes_app/Gui/Pages/Report/Sidebar/Basic/Groups/Export.qml +++ b/src/easyshapes_app/Gui/Pages/Report/Sidebar/Basic/Groups/Export.qml @@ -7,10 +7,10 @@ import QtQuick.Controls import QtQuick.Dialogs import QtCore -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Logic as EaLogic +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Globals as EaGlobals +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Logic as EaLogic import Gui.Globals as Globals diff --git a/src/easyshapes_app/Gui/Pages/Report/Sidebar/Basic/Layout.qml b/src/easyshapes_app/Gui/Pages/Report/Sidebar/Basic/Layout.qml index 472d957..059d6de 100644 --- a/src/easyshapes_app/Gui/Pages/Report/Sidebar/Basic/Layout.qml +++ b/src/easyshapes_app/Gui/Pages/Report/Sidebar/Basic/Layout.qml @@ -4,8 +4,8 @@ import QtQuick -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents import Gui.Globals as Globals diff --git a/src/easyshapes_app/Gui/Pages/Report/Sidebar/Extra/Groups/Empty.qml b/src/easyshapes_app/Gui/Pages/Report/Sidebar/Extra/Groups/Empty.qml index 9f47804..4401fe7 100644 --- a/src/easyshapes_app/Gui/Pages/Report/Sidebar/Extra/Groups/Empty.qml +++ b/src/easyshapes_app/Gui/Pages/Report/Sidebar/Extra/Groups/Empty.qml @@ -4,8 +4,8 @@ import QtQuick -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Elements as EaElements +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements Column {} diff --git a/src/easyshapes_app/Gui/Pages/Report/Sidebar/Extra/Layout.qml b/src/easyshapes_app/Gui/Pages/Report/Sidebar/Extra/Layout.qml index ef2a2ed..7219c46 100644 --- a/src/easyshapes_app/Gui/Pages/Report/Sidebar/Extra/Layout.qml +++ b/src/easyshapes_app/Gui/Pages/Report/Sidebar/Extra/Layout.qml @@ -5,8 +5,8 @@ import QtQuick import QtQuick.Controls -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents import Gui.Globals as Globals diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Layout.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Layout.qml index ac5e6e4..66b000f 100644 --- a/src/easyshapes_app/Gui/Pages/SampleModel/Layout.qml +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Layout.qml @@ -5,36 +5,70 @@ import QtQuick import QtQuick.Controls -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Globals as EaGlobals +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents import Gui.Globals as Globals EaComponents.ContentPage { + id: root + + // Dynamic main-area tabs. The window set is built from backend state and fed + // to the TabBar/SwipeView through Repeaters, so the conditional window (Lattice) + // is genuinely INSERTED and REMOVED rather than hidden in place — + // no reserved empty slot/gap. The two Repeaters share `mainAreaModel`, so the + // tabs and views stay index-aligned. Rebuilds when the structure type or the + // model type changes. + // + // LIMITATION: EaComponents.MainContent doesn't expose its TabBar.currentIndex, + // so when a conditional window is inserted/removed the selected index can shift. + // Preserving the selection across edits needs a key-aware container — + // see the proposed framework component (a model-driven MainContent exposing + // `currentKey`). The `key` field below is carried for that future container. + readonly property var mainAreaModel: { + let list = [] + list.push({ key: "shape", label: qsTr("Shape"), source: "MainArea/Shape.qml" }) + list.push({ key: "components", label: qsTr("Components"), source: "MainArea/Components.qml" }) + if (Globals.BackendWrapper.sampleModelCurrentType === "Lattice") + list.push({ key: "lattice", label: qsTr("Lattice"), source: "MainArea/Lattice.qml" }) + return list + } mainView: EaComponents.MainContent { tabs: [ - EaElements.TabButton { text: qsTr('GraphsView') } + Repeater { + model: root.mainAreaModel + EaElements.TabButton { + text: root.mainAreaModel[index].label + } + } ] items: [ - Loader { source: 'MainArea/GraphsView.qml' } + Repeater { + model: root.mainAreaModel + Loader { + source: root.mainAreaModel[index].source + } + } ] } sideBar: EaComponents.SideBar { tabs: [ - EaElements.TabButton { text: qsTr('Basic controls') } + EaElements.TabButton { text: qsTr("Basic controls") }, + EaElements.TabButton { text: qsTr("Advanced controls") } ] items: [ - Loader { source: 'Sidebar/Basic/Layout.qml' } + Loader { source: "Sidebar/Basic/Layout.qml" }, + Loader { source: "Sidebar/Advanced/Layout.qml" } ] - continueButton.text: qsTr('Continue') + continueButton.text: qsTr("Continue") continueButton.onClicked: { console.debug(`Clicking '${continueButton.text}' button ::: ${this}`) diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/BaseMol3dQuick.qml b/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/BaseMol3dQuick.qml new file mode 100644 index 0000000..e3dfc40 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/BaseMol3dQuick.qml @@ -0,0 +1,501 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick3D + +import EasyApplication.Gui.Style as EaStyle + + +// 3D molecular structure viewer (engine: native Qt Quick 3D, no WebEngine/JS). +// +// App-local for now (kept in shapes-app rather than promoted to EasyApp until +// the design settles). Data-driven: the caller supplies a parsed atom list in +// `atoms` — [{element, x, y, z}] in Ångström, ideally centred on the origin. +// Each atom is drawn as a CPK-coloured sphere sized by its van der Waals radius. +// An optional orientation arrow runs from atom[vectorStart] to atom[vectorEnd]. +// Tap an atom to label it (index + element); tap empty space to clear. +// Left-drag to orbit, right-drag to pan, wheel to zoom. +// +// Bonds (sticks) are not drawn yet — atoms only for now. +Rectangle { + id: root + + // Parsed atoms: [{element: "C", x, y, z}] in Å, centred near the origin. + property var atoms: [] + // Sphere radius = atomScale * vdW-radius(element), in scene units (= Å). + property real atomScale: 0.4 + // Orientation vector: arrow atom[vectorStart] -> atom[vectorEnd] (0-based, + // clamped). -1 means "no vector". + property int vectorStart: -1 + property int vectorEnd: -1 + property color vectorColor: EaStyle.Colors.themeForeground + property real vectorRadius: 0.25 + // Right-button pan sensitivity multiplier (1.0 = baseline). + property real panSpeed: 2.0 + // Left-drag rotation sensitivity, in degrees per pixel (trackball). + property real rotateSpeed: 0.6 + + color: EaStyle.Colors.mainContentBackground + + // CPK-ish colours and van der Waals radii (Å) for the common elements; + // anything else falls back to magenta / 1.6 Å so it stays visible. + readonly property var _cpk: ({ + "H": { c: "#ffffff", r: 1.20 }, + "C": { c: "#909090", r: 1.70 }, + "N": { c: "#3050f8", r: 1.55 }, + "O": { c: "#ff0d0d", r: 1.52 }, + "P": { c: "#ff8000", r: 1.80 }, + "S": { c: "#ffff30", r: 1.80 }, + "F": { c: "#90e050", r: 1.47 }, + "Cl": { c: "#1ff01f", r: 1.75 }, + "Br": { c: "#a62929", r: 1.85 }, + "Na": { c: "#ab5cf2", r: 2.27 }, + "K": { c: "#8f40d4", r: 2.75 }, + "Ca": { c: "#3dff00", r: 2.31 }, + "Mg": { c: "#8aff00", r: 1.73 } + }) + + function _info(element) { + return _cpk[element] !== undefined ? _cpk[element] : { c: "#ff40ff", r: 1.6 } + } + + // --- View framing + derived geometry, recomputed when atoms change ------- + + property real _camDist: 600 // camera distance (fits the molecule) + property real _boundR: 1 // molecule bounding radius (max dist from centre) + + // Orientation arrow, in scene space (local +Y of _arrowNode = direction). + property bool _hasVector: false + property vector3d _vecPos: Qt.vector3d(0, 0, 0) + property quaternion _vecRot: Qt.quaternion(1, 0, 0, 0) + property real _vecLen: 0 + + onAtomsChanged: _recompute() + onVectorStartChanged: _updateVector() + onVectorEndChanged: _updateVector() + + function _recompute() { + let maxR = 1 + for (let i = 0; i < atoms.length; ++i) { + const a = atoms[i] + const d = Math.sqrt(a.x * a.x + a.y * a.y + a.z * a.z) + if (d > maxR) + maxR = d + } + _boundR = maxR + _fitMolecule() + _updateVector() + } + + // Zoom so the molecule (bounding sphere, radius _boundR) fills 0.9 of the + // view. With a horizontal FOV the visible width is 2*dist*tan(fov/2) and the + // visible height is that / aspect, so fit to whichever is smaller. + function _fitMolecule() { + if (typeof camera === "undefined" || view.height <= 0) + return + const aspect = view.width / view.height + const halfTan = Math.tan(camera.fieldOfView * Math.PI / 180 / 2) + _camDist = _boundR * Math.max(1, aspect) / (0.9 * halfTan) + } + + // Quaternion rotating local +Y onto direction `d`. + function _quatFromY(d) { + const up = Qt.vector3d(0, 1, 0) + const n = d.normalized() + const c = Math.max(-1, Math.min(1, up.dotProduct(n))) + if (c > 0.99999) + return Qt.quaternion(1, 0, 0, 0) + if (c < -0.99999) + return Qt.quaternion(0, 0, 0, 1) // 180° about Z + const axis = up.crossProduct(n).normalized() + const ang = Math.acos(c) + const s = Math.sin(ang / 2) + return Qt.quaternion(Math.cos(ang / 2), axis.x * s, axis.y * s, axis.z * s) + } + + // Quaternion for a rotation of `deg` degrees about world axis `axis`. + function _axisAngle(axis, deg) { + const a = axis.normalized() + const r = deg * Math.PI / 180 + const s = Math.sin(r / 2) + return Qt.quaternion(Math.cos(r / 2), a.x * s, a.y * s, a.z * s) + } + + // Hamilton product a * b (applies b, then a). + function _qmul(a, b) { + return Qt.quaternion( + a.scalar * b.scalar - a.x * b.x - a.y * b.y - a.z * b.z, + a.scalar * b.x + a.x * b.scalar + a.y * b.z - a.z * b.y, + a.scalar * b.y - a.x * b.z + a.y * b.scalar + a.z * b.x, + a.scalar * b.z + a.x * b.y - a.y * b.x + a.z * b.scalar) + } + + // Quaternion from an orthonormal basis given as the world directions of the + // local +X, +Y, +Z axes (columns of the rotation matrix). + function _matToQuat(cx, cy, cz) { + const m00 = cx.x, m10 = cx.y, m20 = cx.z + const m01 = cy.x, m11 = cy.y, m21 = cy.z + const m02 = cz.x, m12 = cz.y, m22 = cz.z + const tr = m00 + m11 + m22 + let w, x, y, z, s + if (tr > 0) { + s = Math.sqrt(tr + 1.0) * 2 + w = 0.25 * s; x = (m21 - m12) / s; y = (m02 - m20) / s; z = (m10 - m01) / s + } else if (m00 > m11 && m00 > m22) { + s = Math.sqrt(1.0 + m00 - m11 - m22) * 2 + w = (m21 - m12) / s; x = 0.25 * s; y = (m01 + m10) / s; z = (m02 + m20) / s + } else if (m11 > m22) { + s = Math.sqrt(1.0 + m11 - m00 - m22) * 2 + w = (m02 - m20) / s; x = (m01 + m10) / s; y = 0.25 * s; z = (m12 + m21) / s + } else { + s = Math.sqrt(1.0 + m22 - m00 - m11) * 2 + w = (m10 - m01) / s; x = (m02 + m20) / s; y = (m12 + m21) / s; z = 0.25 * s + } + return Qt.quaternion(w, x, y, z) + } + + // Camera rotation that frames the vector `dir` side-on (forward ⟂ dir, so it + // shows full length), running left->right (mint->mext) and tilted 30° up. + function _viewRotForVector(dir) { + const d = dir.normalized() + let a = Qt.vector3d(0, 0, 1) + if (Math.abs(d.dotProduct(a)) > 0.9) + a = Qt.vector3d(0, 1, 0) + const f = d.crossProduct(a).normalized() // look axis, ⟂ dir + const p = f.crossProduct(d).normalized() // in-plane axis ⟂ dir + const th = 30 * Math.PI / 180 + const r = d.times(Math.cos(th)).minus(p.times(Math.sin(th))) // screen right + const u = d.times(Math.sin(th)).plus(p.times(Math.cos(th))) // screen up + const z = r.crossProduct(u).normalized() // local +Z + return _matToQuat(r, u, z) + } + + function _updateVector() { + _hasVector = false + const n = atoms.length + if (n === 0 || vectorStart < 0 || vectorEnd < 0) + return + const si = Math.max(0, Math.min(vectorStart, n - 1)) + const ei = Math.max(0, Math.min(vectorEnd, n - 1)) + if (si === ei) + return + const s = Qt.vector3d(atoms[si].x, atoms[si].y, atoms[si].z) + const e = Qt.vector3d(atoms[ei].x, atoms[ei].y, atoms[ei].z) + const dir = e.minus(s) + _vecLen = dir.length() + _vecPos = s + _vecRot = _quatFromY(dir) + _hasVector = _vecLen > 0 + // Initial view: vector side-on, running left->right (mint->mext) and + // tilted 30° up. The user can orbit away after. + if (_hasVector && typeof originNode !== "undefined") + originNode.rotation = _viewRotForVector(dir) + } + + // --- Atom labels --------------------------------------------------------- + // Hover shows a transient label at the cursor; clicking an atom pins its + // label so it stays, and pinned labels follow the atom as the view rotates. + + property int _hoverIndex: -1 + property real _hoverX: 0 + property real _hoverY: 0 + property int _pinnedIndex: -1 // single clicked atom label (-1 = none) + + function _elementAt(i) { + return (i >= 0 && i < atoms.length) ? atoms[i].element : "" + } + + // Always-on labels for the mint/mext atoms (the vector endpoints), using the + // same clamped indices as the orientation arrow. + readonly property var _markers: { + const n = atoms.length + const out = [] + if (n > 0 && vectorStart >= 0) + out.push({ i: Math.max(0, Math.min(vectorStart, n - 1)), tag: "mint" }) + if (n > 0 && vectorEnd >= 0) + out.push({ i: Math.max(0, Math.min(vectorEnd, n - 1)), tag: "mext" }) + return out + } + + View3D { + id: view + anchors.fill: parent + camera: camera + + // Keep the 0.9 molecule fit correct when the viewport is resized. + onWidthChanged: root._fitMolecule() + onHeightChanged: root._fitMolecule() + + environment: SceneEnvironment { + clearColor: root.color + backgroundMode: SceneEnvironment.Color + antialiasingMode: SceneEnvironment.MSAA + antialiasingQuality: SceneEnvironment.High + } + + // Camera orbits this node (kept at the origin = molecule centroid). + Node { + id: originNode + PerspectiveCamera { + id: camera + z: root._camDist + clipFar: 100000 + // Horizontal FOV so the visible world-width at the molecule is + // 2*dist*tan(fov/2), independent of the viewport aspect — this + // lets us zoom so the vector length matches the view width. + fieldOfViewOrientation: PerspectiveCamera.Horizontal + } + } + + // Three lights spread over the sphere: azimuths ~120° apart AND + // different elevations, so they don't share one plane (which produced + // the "melon" banding). Tune the angles to taste. + DirectionalLight { eulerRotation.x: -25; eulerRotation.y: -70; brightness: 0.6 } + DirectionalLight { eulerRotation.x: 30; eulerRotation.y: 50; brightness: 0.4 } + DirectionalLight { eulerRotation.x: -55; eulerRotation.y: 170; brightness: 0.2 } + + // Atoms. + Node { + id: moleculeNode + Repeater3D { + model: root.atoms + delegate: Model { + required property int index + required property var modelData + readonly property var info: root._info(modelData.element) + property int atomIndex: index + property string element: modelData.element + source: "#Sphere" + pickable: true + position: Qt.vector3d(modelData.x, modelData.y, modelData.z) + // #Sphere built-in mesh has radius 50 units → scale = R/50. + scale: { + const s = root.atomScale * info.r / 50 + return Qt.vector3d(s, s, s) + } + materials: PrincipledMaterial { + baseColor: info.c + roughness: 0.45 + // 0.15 ambient floor: Qt Quick 3D has no global ambient + // term (that needs an IBL probe), so add a 15% self-lit + // emissive in the atom's own colour instead. + property color _base: info.c + emissiveFactor: Qt.vector3d(_base.r * 0.15, _base.g * 0.15, _base.b * 0.15) + } + } + } + } + + // Orientation arrow atom[vectorStart] -> atom[vectorEnd]: shaft + tip, + // built along local +Y then placed/rotated onto the vector. + Node { + id: arrowNode + visible: root._hasVector + position: root._vecPos + rotation: root._vecRot + + // Tip length as a fraction of the whole vector (0.3x the previous + // 0.075). The cone apex lands on the mext atom centre (total length + // == _vecLen); the shaft runs further into the tip by 0.5x the tip + // length so the rod reaches deeper towards mext. + readonly property real tipLen: root._vecLen * 0.0675 + + Model { // shaft + source: "#Cylinder" + readonly property real len: root._vecLen - arrowNode.tipLen * 0.5 + position: Qt.vector3d(0, len / 2, 0) + // Shaft radius halved (vectorRadius / 2). + scale: Qt.vector3d(root.vectorRadius / 100, len / 100, root.vectorRadius / 100) + // Unlit/flat so the arrow reads as an annotation, not a shaded + // atom (it can otherwise be mistaken for an orange/yellow atom). + materials: PrincipledMaterial { + baseColor: root.vectorColor + lighting: PrincipledMaterial.NoLighting + } + } + Model { // tip cone + source: "#Cone" + readonly property real len: arrowNode.tipLen + // Elongated 3x; shifted towards mint by one pre-elongation + // length (= tipLen / 3). + position: Qt.vector3d(0, root._vecLen - len / 2 - arrowNode.tipLen / 3, 0) + scale: Qt.vector3d(root.vectorRadius * 2.4 / 50, len / 100, root.vectorRadius * 2.4 / 50) + materials: PrincipledMaterial { + baseColor: root.vectorColor + lighting: PrincipledMaterial.NoLighting + } + } + } + + // Hover an atom to show a transient label at the cursor. + HoverHandler { + id: hoverHandler + acceptedDevices: PointerDevice.Mouse + onPointChanged: { + if (!hovered) { + root._hoverIndex = -1 + return + } + const p = point.position + const res = view.pick(p.x, p.y) + root._hoverIndex = (res.objectHit && res.objectHit.atomIndex !== undefined) + ? res.objectHit.atomIndex : -1 + root._hoverX = p.x + root._hoverY = p.y + } + onHoveredChanged: if (!hovered) root._hoverIndex = -1 + } + + // Click an atom to pin its label (only one at a time — replaces any + // previous; clicking the same one unpins it). Clicking empty space + // clears it. The mint/mext labels are separate and always shown. + TapHandler { + onTapped: function (eventPoint) { + const p = eventPoint.position + const res = view.pick(p.x, p.y) + if (res.objectHit && res.objectHit.atomIndex !== undefined) { + const idx = res.objectHit.atomIndex + root._pinnedIndex = (root._pinnedIndex === idx) ? -1 : idx + } else { + root._pinnedIndex = -1 + } + } + } + + // Left-drag trackball rotation. Each mouse delta rotates about the + // CURRENT screen axes (camera right/up, read as world-space basis from + // originNode) and is composed onto the current rotation. Because the + // axes always follow the view, left-right stays consistent at any + // orientation — no gimbal flip when tumbling past the poles. + DragHandler { + target: null + acceptedButtons: Qt.LeftButton + property real _lx: 0 + property real _ly: 0 + onActiveChanged: { _lx = 0; _ly = 0 } + onTranslationChanged: { + const dx = translation.x - _lx + const dy = translation.y - _ly + _lx = translation.x + _ly = translation.y + const qYaw = root._axisAngle(originNode.up, -dx * root.rotateSpeed) + const qPitch = root._axisAngle(originNode.right, -dy * root.rotateSpeed) + const dq = root._qmul(qYaw, qPitch) + originNode.rotation = root._qmul(dq, originNode.rotation) + } + } + + // Right-drag pan (panSpeed x baseline): move the rig in the view plane. + DragHandler { + target: null + acceptedButtons: Qt.RightButton + property real _lx: 0 + property real _ly: 0 + onActiveChanged: { _lx = 0; _ly = 0 } + onTranslationChanged: { + const dx = translation.x - _lx + const dy = translation.y - _ly + _lx = translation.x + _ly = translation.y + const k = root._camDist * 0.0022 * root.panSpeed + originNode.position = originNode.position + .minus(originNode.right.times(dx * k)) + .plus(originNode.up.times(dy * k)) + } + } + + // Wheel zoom: change the camera distance (camera.z is bound to it). + WheelHandler { + acceptedDevices: PointerDevice.Mouse + onWheel: function (ev) { + const factor = ev.angleDelta.y > 0 ? 0.9 : 1.1 + root._camDist = Math.max(2, root._camDist * factor) + } + } + } + + // Single clicked-atom label, projected so it follows the molecule as it + // rotates/zooms/pans. Referencing camera.scenePosition makes the projection + // re-evaluate whenever the view changes. + Rectangle { + id: pin + readonly property vector3d _atomPos: { + const a = root.atoms[root._pinnedIndex] + return a ? Qt.vector3d(a.x, a.y, a.z) : Qt.vector3d(0, 0, 0) + } + readonly property vector3d _screen: { + const _ = camera.scenePosition // dependency: re-project on view change + if (root._pinnedIndex < 0) + return Qt.vector3d(0, 0, -1) // nothing pinned: skip projection + return view.mapFrom3DScene(_atomPos) + } + visible: root._pinnedIndex >= 0 && _screen.z > 0 + x: Math.round(_screen.x) + 8 + y: Math.round(_screen.y) + 8 + width: pinText.implicitWidth + 10 + height: pinText.implicitHeight + 6 + radius: 3 + color: EaStyle.Colors.themeBackground + Text { + id: pinText + anchors.centerIn: parent + color: EaStyle.Colors.themeForeground + font.pixelSize: 12 + text: root._pinnedIndex + " (" + root._elementAt(root._pinnedIndex) + ")" + } + } + + // Always-on labels for the mint/mext atoms, projected so they follow the + // molecule as it rotates/zooms/pans (gold to match the arrow). + Repeater { + model: root._markers + delegate: Rectangle { + id: marker + required property var modelData + readonly property vector3d _atomPos: { + const a = root.atoms[modelData.i] + return a ? Qt.vector3d(a.x, a.y, a.z) : Qt.vector3d(0, 0, 0) + } + readonly property vector3d _screen: { + const _ = camera.scenePosition // dependency: re-project on view change + return view.mapFrom3DScene(_atomPos) + } + visible: _screen.z > 0 + x: Math.round(_screen.x) + 8 + y: Math.round(_screen.y) + 8 + width: markerText.implicitWidth + 10 + height: markerText.implicitHeight + 6 + radius: 3 + color: EaStyle.Colors.themeBackground + Text { + id: markerText + anchors.centerIn: parent + color: EaStyle.Colors.themeForeground + font.pixelSize: 12 + text: marker.modelData.tag + " " + marker.modelData.i + + " (" + root._elementAt(marker.modelData.i) + ")" + } + } + } + + // Transient label for the atom currently under the cursor. + Rectangle { + visible: root._hoverIndex >= 0 + x: Math.round(root._hoverX) + 8 + y: Math.round(root._hoverY) + 8 + width: hoverText.implicitWidth + 10 + height: hoverText.implicitHeight + 6 + radius: 3 + color: EaStyle.Colors.themeBackground + Text { + id: hoverText + anchors.centerIn: parent + color: EaStyle.Colors.themeForeground + font.pixelSize: 12 + text: root._hoverIndex + " (" + root._elementAt(root._hoverIndex) + ")" + } + } + +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/ComponentView.qml b/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/ComponentView.qml new file mode 100644 index 0000000..677a3f0 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/ComponentView.qml @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick + +import EasyApplication.Gui.Style as EaStyle + +import Gui.Globals as Globals + + +// Main-area window: per-component atomistic depiction. Shows the selected +// component on its own in the Qt Quick 3D molecular viewer (parsed atoms drawn +// as CPK spheres). `componentIndex` / `componentName` are set by the page +// Loader. The mock returns one shared template molecule. +// +// mint/mext are atom indices; the viewer draws an orientation arrow +// atom[mint] -> atom[mext] over the molecule. +Rectangle { + id: root + + // Row in Globals.BackendWrapper.componentsLoaded this window depicts. + property int componentIndex: -1 + property string componentName: "" + // Atom indices defining the orientation vector drawn over the molecule + // (arrow atom[mint] -> atom[mext]); -1 means "no vector". + property int componentMint: -1 + property int componentMext: -1 + + color: EaStyle.Colors.mainContentBackground + + BaseMol3dQuick { + anchors.fill: parent + atoms: root.componentIndex >= 0 + ? Globals.BackendWrapper.componentStructureAtoms(root.componentIndex) + : [] + vectorStart: root.componentMint + vectorEnd: root.componentMext + } + +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/Components.qml b/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/Components.qml new file mode 100644 index 0000000..2f3e695 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/Components.qml @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements + +import Gui.Globals as Globals + + +// Main-area window: Components. A dropdown selects which loaded component to +// depict (mirroring the component selector in the Advanced tab's Components Files +// group), and the selected component is shown below in a ComponentView. Keeping +// the per-component choice in a ComboBox keeps the page's outer tabs simple. +// DRAFT CONTAINER — per-component rendering not implemented yet. +Rectangle { + id: root + + readonly property var components: Globals.BackendWrapper.componentsLoaded + + color: EaStyle.Colors.mainContentBackground + + // Empty state — no components loaded yet. + EaElements.Label { + anchors.centerIn: parent + visible: root.components.count === 0 + color: EaStyle.Colors.themeForegroundMinor + text: qsTr("No components yet — load or create one in the sidebar.") + } + + // Component selector. Mirrors Advanced › Components Files: dropdown over the + // loaded components by name. The current index is the selection driving the + // ComponentView below. + Column { + id: selectorBlock + + anchors.top: parent.top + anchors.left: parent.left + anchors.margins: EaStyle.Sizes.fontPixelSize * 1.5 + + visible: root.components.count > 0 + + EaElements.Label { + enabled: false + text: qsTr("Component") + } + + EaElements.ComboBox { + id: componentSelector + width: EaStyle.Sizes.fontPixelSize * 20 + textRole: "name" + model: root.components + + // Keep the selection valid as components are added/removed. + Connections { + target: root.components + function onCountChanged() { + if (root.components.count === 0) + componentSelector.currentIndex = -1 + else if (componentSelector.currentIndex < 0) + componentSelector.currentIndex = 0 + else if (componentSelector.currentIndex >= root.components.count) + componentSelector.currentIndex = root.components.count - 1 + } + } + } + } + + // The selected component's depiction. + ComponentView { + anchors.top: selectorBlock.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.topMargin: EaStyle.Sizes.fontPixelSize + + visible: root.components.count > 0 + + readonly property bool hasSelection: componentSelector.currentIndex >= 0 + && componentSelector.currentIndex < root.components.count + readonly property var selectedRow: hasSelection + ? root.components.get(componentSelector.currentIndex) + : null + + componentIndex: componentSelector.currentIndex + componentName: hasSelection ? selectedRow.name : "" + // mint/mext are atom indices; the viewer draws an arrow atom[mint] -> atom[mext]. + componentMint: hasSelection ? selectedRow.mint : -1 + componentMext: hasSelection ? selectedRow.mext : -1 + } + +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/FlatShapeView.qml b/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/FlatShapeView.qml new file mode 100644 index 0000000..6724d93 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/FlatShapeView.qml @@ -0,0 +1,334 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements + +import Gui.Globals as Globals + + +// Shape view for the flat structures (Bilayer / Monolayer). Like the layered +// ring view but with NO curvature: the baselines are straight lines. +// +// - Two leaflets, a bottom one (y = 0) and a top one (y = zsep). `zsep` plays +// the role the shell thickness plays for the vesicle. +// - Each leaflet has N+1 mint->mext vectors (N = present components; the first +// component owns two, mirroring the ring layout), spaced by `dmin`. +// - The top leaflet is offset to the right by 0.5*dmin, and the whole sketch is +// rotated by `rotationDeg`, so it reads like the other (radial) shapes. +// - Bilayer: vectors point INTO the gap (heads on the outer surfaces, tails in +// the middle). Monolayer: `reversed` flips them (heads in the middle). +// - Vector length is the real molecular mint->mext distance (Å) / lengthDivisor. +Item { + id: root + + property var components: Globals.BackendWrapper.componentsLoaded + property var fractions: Globals.BackendWrapper.fractionsModel + + property real dmin: 0.5 + property real zsep: 0.0 + property int nside: 1 // not used for the schematic vector count + property bool reversed: false // Monolayer flips the mint->mext direction + + property real lengthDivisor: 5 + property real rotationDeg: -45 // tilt the whole sketch clockwise, like the radial views + + property bool showComponentNames: false + property bool showDimensions: false + + property color baselineColor: EaStyle.Colors.themeForegroundDisabled + property color vectorColor: EaStyle.Colors.themeForegroundDisabled + property color labelColor: EaStyle.Colors.themeForeground + property color dimColor: EaStyle.Colors.themeForegroundMinor + property color clearColor: EaStyle.Colors.mainContentBackground + + onDminChanged: canvas.requestPaint() + onZsepChanged: canvas.requestPaint() + onReversedChanged: canvas.requestPaint() + onLengthDivisorChanged: canvas.requestPaint() + onRotationDegChanged: canvas.requestPaint() + onShowComponentNamesChanged: canvas.requestPaint() + onShowDimensionsChanged: canvas.requestPaint() + onFractionsChanged: canvas.requestPaint() + onBaselineColorChanged: canvas.requestPaint() + onVectorColorChanged: canvas.requestPaint() + onLabelColorChanged: canvas.requestPaint() + onDimColorChanged: canvas.requestPaint() + onClearColorChanged: canvas.requestPaint() + + function _presentComps() { + const out = [] + const fm = fractions + if (!fm || fm.count === undefined) { + for (let i = 0; i < components.count; ++i) + out.push(i) + return out + } + const n = Math.min(fm.count, components.count) + for (let i = 0; i < n; ++i) { + const row = fm.get(i) + if (row && row.present && row.fracs > 0) + out.push(i) + } + return out + } + + function _vectorLengthNm(compIndex) { + if (compIndex < 0 || compIndex >= components.count) + return 0 + const row = components.get(compIndex) + if (!row || row.mint < 0 || row.mext < 0) + return 0 + const atoms = Globals.BackendWrapper.componentStructureAtoms(compIndex) + const n = atoms ? atoms.length : 0 + if (n === 0) + return 0 + const mi = Math.max(0, Math.min(row.mint, n - 1)) + const me = Math.max(0, Math.min(row.mext, n - 1)) + const a = atoms[mi], b = atoms[me] + const dx = b.x - a.x, dy = b.y - a.y, dz = b.z - a.z + const div = root.lengthDivisor > 0 ? root.lengthDivisor : 1 + return Math.sqrt(dx * dx + dy * dy + dz * dz) / 10.0 / div + } + + function _compName(compIndex) { + if (compIndex >= 0 && compIndex < components.count) { + const row = components.get(compIndex) + if (row && row.name) + return row.name + } + return qsTr("Comp %1").arg(compIndex + 1) + } + + Connections { + target: root.components + function onCountChanged() { canvas.requestPaint() } + function onDataChanged(topLeft, bottomRight, roles) { canvas.requestPaint() } + } + Connections { + target: root.fractions + ignoreUnknownSignals: true + function onCountChanged() { canvas.requestPaint() } + function onDataChanged(topLeft, bottomRight, roles) { canvas.requestPaint() } + } + + Canvas { + id: canvas + anchors.fill: parent + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + + onPaint: { + const ctx = getContext("2d") + ctx.reset() + ctx.clearRect(0, 0, width, height) + + const comps = root._presentComps() + const m = comps.length + if (m === 0) + return + + const d = Math.max(root.dmin, 1e-3) + const zsep = Math.max(0, root.zsep) + const phi = root.rotationDeg * Math.PI / 180 + const cphi = Math.cos(phi), sphi = Math.sin(phi) + + // N+1 vectors per leaflet (slot k -> comps[k % m]); centre the row. + const halfSpan = m * d / 2 + // Bottom leaflet points up (+y), top points down (-y) and is shifted + // right by half a dmin. + const leaflets = [{ y: 0, nrm: 1, xs: 0 }, { y: zsep, nrm: -1, xs: 0.5 * d }] + + // ---- bounds over rotated world points ---- + let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity + function track(x, y) { + const rx = x * cphi - y * sphi, ry = x * sphi + y * cphi + if (rx < minX) minX = rx + if (rx > maxX) maxX = rx + if (ry < minY) minY = ry + if (ry > maxY) maxY = ry + } + + const vecs = [] + const baselines = [] + for (let l = 0; l < leaflets.length; ++l) { + const lf = leaflets[l] + const x0 = -halfSpan - 0.5 * d + lf.xs + const x1 = halfSpan + 0.5 * d + lf.xs + baselines.push({ y: lf.y, x0: x0, x1: x1 }) + track(x0, lf.y); track(x1, lf.y) + for (let k = 0; k <= m; ++k) { + const x = k * d - halfSpan + lf.xs + const compIndex = comps[k % m] + let vlen = root._vectorLengthNm(compIndex) + if (vlen <= 1e-3) + vlen = d + const baseY = lf.y + const freeY = lf.y + lf.nrm * vlen + const from = root.reversed ? { x: x, y: freeY } : { x: x, y: baseY } + const to = root.reversed ? { x: x, y: baseY } : { x: x, y: freeY } + vecs.push({ from: from, to: to, bx: x, by: baseY, fx: x, fy: freeY, + compIndex: compIndex }) + track(x, baseY); track(x, freeY) + } + } + + // dimension anchor points (so they are framed too) + const dOff = 0.4 * d + const showDmin = root.showDimensions + const showZsep = root.showDimensions && zsep > 0 + if (showDmin) { track(-halfSpan, -dOff); track(-halfSpan + d, -dOff) } + const zsepX = baselines[1].x1 + dOff // right end (bottom after the CW rotation) + if (showZsep) { track(zsepX, 0); track(zsepX, zsep) } + + // ---- world -> screen (rotate, fit, flip Y) ---- + const margin = 0.82 + const spanX = Math.max(maxX - minX, 1e-3) + const spanY = Math.max(maxY - minY, 1e-3) + const scale = Math.min((width * margin) / spanX, (height * margin) / spanY) + const cx = (minX + maxX) / 2 + const cy = (minY + maxY) / 2 + function S(x, y) { + const rx = x * cphi - y * sphi, ry = x * sphi + y * cphi + return Qt.point((rx - cx) * scale + width / 2, + height / 2 - (ry - cy) * scale) + } + + const headAng = Math.PI / 7 + const fontPx = EaStyle.Sizes.fontPixelSize + const compFontPx = Math.round(fontPx * 0.7) + const dimFont = Math.max(10, fontPx * 0.8) + + function drawLabel(cxp, cyp, angle, text, px, bg, fg) { + let aa = angle + if (aa > Math.PI / 2) aa -= Math.PI + else if (aa < -Math.PI / 2) aa += Math.PI + ctx.save() + ctx.translate(cxp, cyp) + ctx.rotate(aa) + ctx.font = px + "px sans-serif" + ctx.textAlign = "center" + ctx.textBaseline = "middle" + const w = ctx.measureText(text).width + ctx.fillStyle = bg + ctx.fillRect(-w / 2 - 3, -px * 0.7, w + 6, px * 1.4) + ctx.fillStyle = fg + ctx.fillText(text, 0, 0) + ctx.restore() + } + function dimHead(tx, ty, ang) { + const hl = 7, ha = Math.PI / 7 + ctx.beginPath() + ctx.moveTo(tx, ty) + ctx.lineTo(tx - hl * Math.cos(ang - ha), ty - hl * Math.sin(ang - ha)) + ctx.lineTo(tx - hl * Math.cos(ang + ha), ty - hl * Math.sin(ang + ha)) + ctx.closePath() + ctx.fill() + } + + // ---- baselines ---- + ctx.lineWidth = 2 + ctx.strokeStyle = root.baselineColor + for (let b = 0; b < baselines.length; ++b) { + const p0 = S(baselines[b].x0, baselines[b].y) + const p1 = S(baselines[b].x1, baselines[b].y) + ctx.beginPath() + ctx.moveTo(p0.x, p0.y); ctx.lineTo(p1.x, p1.y) + ctx.stroke() + } + + // ---- vectors ---- + for (let k = 0; k < vecs.length; ++k) { + const v = vecs[k] + const a = S(v.from.x, v.from.y) + const b = S(v.to.x, v.to.y) + const shaftLen = Math.hypot(b.x - a.x, b.y - a.y) + const headLen = Math.max(8, Math.min(shaftLen * 0.3, 18)) + const ang = Math.atan2(b.y - a.y, b.x - a.x) + const tbx = b.x - headLen * Math.cos(ang) + const tby = b.y - headLen * Math.sin(ang) + + ctx.lineWidth = 3 + ctx.strokeStyle = root.vectorColor + ctx.beginPath() + ctx.moveTo(a.x, a.y); ctx.lineTo(tbx, tby) + ctx.stroke() + + ctx.fillStyle = root.vectorColor + ctx.beginPath() + ctx.moveTo(b.x, b.y) + ctx.lineTo(b.x - headLen * Math.cos(ang - headAng), + b.y - headLen * Math.sin(ang - headAng)) + ctx.lineTo(b.x - headLen * Math.cos(ang + headAng), + b.y - headLen * Math.sin(ang + headAng)) + ctx.closePath() + ctx.fill() + + const baseS = S(v.bx, v.by) + ctx.beginPath() + ctx.arc(baseS.x, baseS.y, 3.5, 0, 2 * Math.PI) + ctx.fill() + + if (root.showComponentNames) { + const freeS = S(v.fx, v.fy) + const vAng = Math.atan2(freeS.y - baseS.y, freeS.x - baseS.x) + drawLabel((baseS.x + freeS.x) / 2, (baseS.y + freeS.y) / 2, vAng, + root._compName(v.compIndex), + compFontPx, Qt.rgba(1, 1, 1, 0.5), root.labelColor) + } + } + + // ---- dimensions ---- + if (showDmin) { + ctx.strokeStyle = root.dimColor + ctx.fillStyle = root.dimColor + ctx.lineWidth = 1 + // dmin between the first two bottom-leaflet vectors, offset to the + // outer side (away from the gap the vectors point into). + const a0 = S(-halfSpan, 0), e0 = S(-halfSpan, -dOff) + const a1 = S(-halfSpan + d, 0), e1 = S(-halfSpan + d, -dOff) + ctx.beginPath() + ctx.moveTo(a0.x, a0.y); ctx.lineTo(e0.x, e0.y) + ctx.moveTo(a1.x, a1.y); ctx.lineTo(e1.x, e1.y) + ctx.moveTo(e0.x, e0.y); ctx.lineTo(e1.x, e1.y) + ctx.stroke() + const dAng = Math.atan2(e1.y - e0.y, e1.x - e0.x) + dimHead(e0.x, e0.y, dAng + Math.PI) + dimHead(e1.x, e1.y, dAng) + drawLabel((e0.x + e1.x) / 2, (e0.y + e1.y) / 2, dAng, + "dmin " + d.toFixed(2) + " nm", + dimFont, root.clearColor, root.dimColor) + } + if (showZsep) { + ctx.strokeStyle = root.dimColor + ctx.fillStyle = root.dimColor + ctx.lineWidth = 1 + // zsep between the two baselines, off the right end. + const xz = zsepX + const b0 = S(baselines[0].x1, 0), w0 = S(xz, 0) + const b1 = S(baselines[1].x1, zsep), w1 = S(xz, zsep) + ctx.beginPath() + ctx.moveTo(b0.x, b0.y); ctx.lineTo(w0.x, w0.y) + ctx.moveTo(b1.x, b1.y); ctx.lineTo(w1.x, w1.y) + ctx.moveTo(w0.x, w0.y); ctx.lineTo(w1.x, w1.y) + ctx.stroke() + const zAng = Math.atan2(w1.y - w0.y, w1.x - w0.x) + dimHead(w0.x, w0.y, zAng + Math.PI) + dimHead(w1.x, w1.y, zAng) + drawLabel((w0.x + w1.x) / 2, (w0.y + w1.y) / 2, zAng, + "Zsep " + zsep.toFixed(2) + " nm", + dimFont, root.clearColor, root.dimColor) + } + } + } + + EaElements.Label { + anchors.centerIn: parent + visible: !root.components || root.components.count === 0 + color: EaStyle.Colors.themeForegroundMinor + text: qsTr("No components yet — load or create one in the sidebar.") + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/GraphsView.qml b/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/GraphsView.qml index 9f2fd2d..1aa8756 100644 --- a/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/GraphsView.qml +++ b/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/GraphsView.qml @@ -5,8 +5,8 @@ import QtQuick import QtGraphs -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Elements as EaElements +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements import Gui.Globals as Globals @@ -53,7 +53,7 @@ GraphsView { color: EaStyle.Colors.chartLabels } - titleText: 'x' + titleText: "x" min: 0 max: 100 } @@ -68,7 +68,7 @@ GraphsView { color: EaStyle.Colors.chartLabels } - titleText: 'y' + titleText: "y" min: -2 max: 2 } @@ -76,7 +76,7 @@ GraphsView { // lineSeries LineSeries { - color: 'red' + color: "red" XYPoint { x: 0; y: -1 } XYPoint { x: 50; y: 1.5 } diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/Lattice.qml b/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/Lattice.qml new file mode 100644 index 0000000..9c598fd --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/Lattice.qml @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements + + +// Main-area window: lattice depiction — size and placement of the lattice +// units. Shown only for the Lattice type. +// DRAFT CONTAINER — rendering not implemented yet. +Rectangle { + + color: EaStyle.Colors.mainContentBackground + + Column { + anchors.centerIn: parent + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.Label { + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: EaStyle.Sizes.fontPixelSize * 1.5 + color: EaStyle.Colors.themeForegroundMinor + text: qsTr("Lattice") + } + + EaElements.Label { + anchors.horizontalCenter: parent.horizontalCenter + color: EaStyle.Colors.themeForegroundMinor + text: qsTr("Placeholder — to be implemented") + } + + EaElements.Label { + anchors.horizontalCenter: parent.horizontalCenter + color: EaStyle.Colors.themeForegroundMinor + text: qsTr("Will show lattice unit size and placement.") + } + } + +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/RingShapeView.qml b/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/RingShapeView.qml new file mode 100644 index 0000000..dfe1db2 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/RingShapeView.qml @@ -0,0 +1,587 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQml + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements + +import Gui.Globals as Globals + + +// Shape view for ring-based structures. A simple 2D schematic focused on the +// per-component mint -> mext orientation vectors laid out along curved baselines. +// +// It draws one OR MORE concentric rings, all sharing the same centre: +// - Ring structure: a single ring from `dmin` / `rmin` / `rev`. +// - Ball structure: one ring per layer (the `layers` model). Each layer has +// its own dmin / rmin, so the rings are concentric arcs at growing radii. +// Layers alternate orientation: counting from 1, even-numbered layers +// (2, 4, ...) are reversed by default; `rev` flips the parity. +// +// Layout rules per ring: +// - N loaded components produce N+1 vectors. The first component owns TWO +// vectors (one at each end of the baseline); every other component owns one. +// Slot k (k = 0..N) belongs to component (k % N). +// - Consecutive vectors are spaced by `dmin` along the baseline arc: +// comp1 _dmin_ comp2 _dmin_ ... _dmin_ compN _dmin_ comp1 +// - The baseline is an arc of radius `rmin`. It starts 0.5*dmin before the +// first vector and ends 0.5*dmin after the last, so its arc length is +// (N+1)*dmin. +// - Each vector is radial (perpendicular to the baseline). If NOT reversed, +// the mint end is tied to the baseline and the arrow points outward (mext +// outer). If reversed, the mext end is tied to the baseline and the arrow +// points inward toward it (mint outer). +// - The drawn length of each vector is the real molecular mint->mext distance: +// |atom[mext] - atom[mint]| in Ångström, converted to nm and shrunk by +// `lengthDivisor`. mint/mext are atom indices into the component's structure. +Item { + id: root + + property var components: Globals.BackendWrapper.componentsLoaded + + // Single-ring parameters (used when `layers` is not set). + property real dmin: 0.5 + property real rmin: 0.25 + property bool rev: false + + // Optional layers model (ListModel of rows with `dmin` / `rmin` roles). When + // set, one ring is drawn per layer and the single-ring dmin/rmin are ignored. + property var layers: null + + // Optional lamellae model (Vesicle). Rows: rmin, innerDmin, outerDmin, shell. + // Each lamella becomes two leaflet rings (a bilayer): inner at `rmin`, outer + // at `rmin + shell/2`, with opposite vector orientation. The shell thickness + // `shell` spans from the inner baseline (rmin) out to the outer shell + // (rmin + shell). Takes precedence over `layers` when set. + property var lamellae: null + + // The real molecular mint->mext vector is long (POPC ~2.6 nm) versus the + // baseline spacing (dmin >= 0.5 nm), so drawn 1:1 the vectors swamp the arc. + // Shrink the schematic vector length by this divisor for legibility; the + // real length is unchanged in the 3D Components viewer. + property real lengthDivisor: 5 + + // Rotate the whole schematic about its centre, degrees clockwise on screen. + property real rotationDeg: 45 + + // Length (nm) of the drawn rmin radius leader. It points along the radius + // (as if from the centre) but is only this long, not the full radius. + property real radiusLeaderNm: 0.3 + + // Toggles (driven by the Shape view's checkboxes). Off by default. + property bool showComponentNames: false + property bool showDimensions: false + + // Theme colours pulled out as properties so a theme switch repaints. + property color baselineColor: EaStyle.Colors.themeForegroundDisabled + property color vectorColor: EaStyle.Colors.themeForegroundDisabled + property color labelColor: EaStyle.Colors.themeForeground + // Blueprint dimension annotations (lines + text), and the canvas background + // used to punch a clear gap behind dimension text. + property color dimColor: EaStyle.Colors.themeForegroundMinor + property color clearColor: EaStyle.Colors.mainContentBackground + + onDminChanged: canvas.requestPaint() + onRminChanged: canvas.requestPaint() + onRevChanged: canvas.requestPaint() + onLayersChanged: canvas.requestPaint() + onLamellaeChanged: canvas.requestPaint() + onLengthDivisorChanged: canvas.requestPaint() + onRotationDegChanged: canvas.requestPaint() + onRadiusLeaderNmChanged: canvas.requestPaint() + onShowComponentNamesChanged: canvas.requestPaint() + onShowDimensionsChanged: canvas.requestPaint() + on_WatchedFractionsChanged: canvas.requestPaint() + onBaselineColorChanged: canvas.requestPaint() + onVectorColorChanged: canvas.requestPaint() + onLabelColorChanged: canvas.requestPaint() + onDimColorChanged: canvas.requestPaint() + onClearColorChanged: canvas.requestPaint() + + // Global component indices that are actually present in a fractions set: + // present === true AND mole ratio (fracs) > 0. With no fractions model, all + // loaded components count as present. + function _presentComps(fracModel) { + const out = [] + if (!fracModel || fracModel.count === undefined) { + for (let i = 0; i < components.count; ++i) + out.push(i) + return out + } + const n = Math.min(fracModel.count, components.count) + for (let i = 0; i < n; ++i) { + const row = fracModel.get(i) + if (row && row.present && row.fracs > 0) + out.push(i) + } + return out + } + + // Ring definitions to draw: [{ dmin, rmin, reversed, showRmin, shell, comps }]. + // - Vesicle (lamellae set): two leaflet rings per lamella. + // - Ball (layers set): one ring per layer. + // - Ring: a single ring from the plain properties. + // `comps` is the list of present component indices for that ring (empty rings + // are dropped). `showRmin` controls the rmin dimension; `shell` (> 0) requests + // the outer-shell arc + shell-thickness dimension. + function _ringDefs() { + if (lamellae && lamellae.count !== undefined) { + void Globals.BackendWrapper.lamellaeFractionsRevision + const out = [] + for (let i = 0; i < lamellae.count; ++i) { + const row = lamellae.get(i) + if (!row) + continue + const R = row.rmin // absolute radius; lamellae overlap if set equal + const T = row.shell + // Symmetric lamellae edit a single (inner) fractions set, so both + // leaflets mirror it; asymmetric leaflets read their own. + const innerComps = _presentComps(Globals.BackendWrapper.lamellaeInnerFractionsModelAt(i)) + const outerComps = row.symmetric + ? innerComps + : _presentComps(Globals.BackendWrapper.lamellaeOuterFractionsModelAt(i)) + // Inner leaflet on the lamella baseline; carries the rmin + shell + // annotations. Outer leaflet sits T/2 further out with reversed + // vectors (the two leaflets point toward the bilayer midplane). + out.push({ dmin: row.innerDmin, rmin: R, reversed: root.rev, + showRmin: true, shell: T, comps: innerComps }) + out.push({ dmin: row.outerDmin, rmin: R + T / 2, reversed: !root.rev, + showRmin: false, comps: outerComps }) + } + return out + } + if (layers && layers.count !== undefined) { + void Globals.BackendWrapper.layersFractionsRevision + const out = [] + for (let i = 0; i < layers.count; ++i) { + const row = layers.get(i) + if (!row) + continue + // Layers counted from 1: even-numbered layers (2, 4, ... = + // indices 1, 3, ...) are reversed by default; `rev` flips parity. + out.push({ dmin: row.dmin, rmin: row.rmin, + reversed: (i % 2 === 1) !== root.rev, showRmin: true, + comps: _presentComps(Globals.BackendWrapper.layersFractionsModelAt(i)) }) + } + return out + } + return [{ dmin: root.dmin, rmin: root.rmin, reversed: root.rev, showRmin: true, + comps: _presentComps(Globals.BackendWrapper.fractionsModel) }] + } + + // Fractions models to watch for repaint (per leaflet/layer/ring). Rebuilt + // when the structure adds/removes rows (revision tokens). + readonly property var _watchedFractions: { + const out = [] + if (lamellae && lamellae.count !== undefined) { + void Globals.BackendWrapper.lamellaeFractionsRevision + for (let i = 0; i < lamellae.count; ++i) { + out.push(Globals.BackendWrapper.lamellaeInnerFractionsModelAt(i)) + out.push(Globals.BackendWrapper.lamellaeOuterFractionsModelAt(i)) + } + } else if (layers && layers.count !== undefined) { + void Globals.BackendWrapper.layersFractionsRevision + for (let i = 0; i < layers.count; ++i) + out.push(Globals.BackendWrapper.layersFractionsModelAt(i)) + } else { + out.push(Globals.BackendWrapper.fractionsModel) + } + return out.filter(Boolean) + } + + // Schematic mint->mext length for a component, in nm. This is the real + // |atom[mext] - atom[mint]| distance (Å) shrunk by `lengthDivisor` for the + // schematic. Returns 0 if the component has no structure or no valid vector. + function _vectorLengthNm(compIndex) { + if (compIndex < 0 || compIndex >= components.count) + return 0 + const row = components.get(compIndex) + if (!row || row.mint < 0 || row.mext < 0) + return 0 + const atoms = Globals.BackendWrapper.componentStructureAtoms(compIndex) + const n = atoms ? atoms.length : 0 + if (n === 0) + return 0 + const mi = Math.max(0, Math.min(row.mint, n - 1)) + const me = Math.max(0, Math.min(row.mext, n - 1)) + const a = atoms[mi], b = atoms[me] + const dx = b.x - a.x, dy = b.y - a.y, dz = b.z - a.z + const div = root.lengthDivisor > 0 ? root.lengthDivisor : 1 + return Math.sqrt(dx * dx + dy * dy + dz * dz) / 10.0 / div // Å -> nm, shrunk + } + + function _compName(compIndex) { + if (compIndex >= 0 && compIndex < components.count) { + const row = components.get(compIndex) + if (row && row.name) + return row.name + } + return qsTr("Comp %1").arg(compIndex + 1) + } + + // Repaint when components are added/removed (count) and when an existing + // row's value is edited in place (dataChanged) — e.g. changing mint/mext in + // the sidebar table, which alters the drawn vector length. + Connections { + target: root.components + function onCountChanged() { canvas.requestPaint() } + function onDataChanged(topLeft, bottomRight, roles) { canvas.requestPaint() } + } + + // Same, for the layers model (Ball): adding/removing layers or editing a + // layer's dmin/rmin must repaint. + Connections { + target: root.layers + ignoreUnknownSignals: true + function onCountChanged() { canvas.requestPaint() } + function onDataChanged(topLeft, bottomRight, roles) { canvas.requestPaint() } + } + + // Same, for the lamellae model (Vesicle). + Connections { + target: root.lamellae + ignoreUnknownSignals: true + function onCountChanged() { canvas.requestPaint() } + function onDataChanged(topLeft, bottomRight, roles) { canvas.requestPaint() } + } + + // One Connections per fractions model in play, so toggling a component's + // presence or editing its mole ratio recomputes the depicted vectors. + Instantiator { + model: root._watchedFractions + delegate: Connections { + required property var modelData + target: modelData + ignoreUnknownSignals: true + function onCountChanged() { canvas.requestPaint() } + function onDataChanged(topLeft, bottomRight, roles) { canvas.requestPaint() } + } + } + + Canvas { + id: canvas + anchors.fill: parent + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + + onPaint: { + const ctx = getContext("2d") + ctx.reset() + ctx.clearRect(0, 0, width, height) + + const N = root.components ? root.components.count : 0 + if (N <= 0) + return + + const defs = root._ringDefs() + if (defs.length === 0) + return + + const rot = root.rotationDeg * Math.PI / 180 + const arcSamples = 80 + + // outward direction for a baseline angle: theta = 0 -> up (+y). + // Increasing theta turns clockwise on screen (+y is up in world). + function outward(theta) { return Qt.point(Math.sin(theta), Math.cos(theta)) } + + // ---- Build all rings in world coords, track bounds ---- + let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity + function track(p) { + if (p.x < minX) minX = p.x + if (p.x > maxX) maxX = p.x + if (p.y < minY) minY = p.y + if (p.y > maxY) maxY = p.y + } + + const geos = [] + for (let g = 0; g < defs.length; ++g) { + const r = Math.max(defs[g].rmin, 1e-3) + const d = Math.max(defs[g].dmin, 1e-3) + const reversed = !!defs[g].reversed + const showRmin = defs[g].showRmin !== false + const shell = defs[g].shell > 0 ? defs[g].shell : 0 + // Present components for this ring; drop the ring if none. + const comps = defs[g].comps || [] + const m = comps.length + if (m === 0) + continue + const Lfull = (m + 1) * d + // Cap the baseline at 0.7 of the full circle it lies on; any + // vector whose anchor falls past the cut-off is not drawn. + const maxLen = 0.7 * 2 * Math.PI * r + const L = Math.min(Lfull, maxLen) + // -L/r/2 centres the (drawn) arc midpoint at the up axis; +rot turns it. + const a0 = -(L / r) / 2 + rot + + const arc = [] + for (let i = 0; i <= arcSamples; ++i) { + const o = outward(a0 + (i / arcSamples) * L / r) + const p = Qt.point(r * o.x, r * o.y) + arc.push(p); track(p) + } + + const vecs = [] + for (let k = 0; k <= m; ++k) { + const s = 0.5 * d + k * d + if (s > L) // anchor past the cut-off baseline: not shown + continue + const o = outward(a0 + s / r) + const compIndex = comps[k % m] + let vlen = root._vectorLengthNm(compIndex) + if (vlen <= 1e-3) + vlen = d + const base = Qt.point(r * o.x, r * o.y) // on baseline + const free = Qt.point((r + vlen) * o.x, (r + vlen) * o.y) // outer end + // Arrow goes mint -> mext. Anchored (baseline) end is mint + // unless reversed, in which case it is mext. + const from = reversed ? free : base // mint + const to = reversed ? base : free // mext (arrowhead here) + vecs.push({ from: from, to: to, base: base, free: free, + compIndex: compIndex }) + track(base); track(free) + } + + // Point on the arc 0.25*dmin before the first vector (for the + // rmin radius leader). + const ro = outward(a0 + (0.25 * d) / r) + const arcMid = Qt.point(r * ro.x, r * ro.y) + + // Vesicle outer shell: an arc at radius r+shell over the same + // angular sweep, plus the radial thickness endpoints (inner + // baseline -> outer shell). The thickness dimension mirrors the + // rmin leader to the far end: 0.25*dmin past the last vector, or + // the cut-off edge of the baseline, whichever comes first. + let shellArc = null, shellInner = null, shellOuter = null + if (shell > 0) { + const rs = r + shell + shellArc = [] + for (let i = 0; i <= arcSamples; ++i) { + const o = outward(a0 + (i / arcSamples) * L / r) + const p = Qt.point(rs * o.x, rs * o.y) + shellArc.push(p); track(p) + } + const sShell = Math.min((N + 0.75) * d, L) + const om = outward(a0 + sShell / r) + shellInner = Qt.point(r * om.x, r * om.y) + shellOuter = Qt.point(rs * om.x, rs * om.y) + } + + geos.push({ r: r, d: d, arc: arc, vecs: vecs, arcMid: arcMid, + showRmin: showRmin, shell: shell, + shellArc: shellArc, shellInner: shellInner, + shellOuter: shellOuter }) + } + + // ---- World -> screen mapping (fit with margin, flip Y) ---- + const margin = 0.82 + const spanX = Math.max(maxX - minX, 1e-3) + const spanY = Math.max(maxY - minY, 1e-3) + const scale = Math.min((width * margin) / spanX, (height * margin) / spanY) + const cx = (minX + maxX) / 2 + const cy = (minY + maxY) / 2 + function S(p) { + return Qt.point((p.x - cx) * scale + width / 2, + height / 2 - (p.y - cy) * scale) + } + const centerS = S(Qt.point(0, 0)) + + const headAng = Math.PI / 7 + const fontPx = EaStyle.Sizes.fontPixelSize + const compFontPx = Math.round(fontPx * 0.7) + + // Text centred at (cxp,cyp) and rotated to run parallel to `angle` + // (kept upright), on a coloured background plate. + function drawLabel(cxp, cyp, angle, text, px, bg, fg) { + let aa = angle + if (aa > Math.PI / 2) aa -= Math.PI + else if (aa < -Math.PI / 2) aa += Math.PI + ctx.save() + ctx.translate(cxp, cyp) + ctx.rotate(aa) + ctx.font = px + "px sans-serif" + ctx.textAlign = "center" + ctx.textBaseline = "middle" + const w = ctx.measureText(text).width + ctx.fillStyle = bg + ctx.fillRect(-w / 2 - 3, -px * 0.7, w + 6, px * 1.4) + ctx.fillStyle = fg + ctx.fillText(text, 0, 0) + ctx.restore() + } + + // ---- Draw a ring's baseline + vectors ---- + function drawPolyline(pts) { + ctx.beginPath() + let p0 = S(pts[0]) + ctx.moveTo(p0.x, p0.y) + for (let i = 1; i < pts.length; ++i) { + const p = S(pts[i]) + ctx.lineTo(p.x, p.y) + } + ctx.stroke() + } + function drawRing(geo) { + // baseline + ctx.lineWidth = 2 + ctx.strokeStyle = root.baselineColor + drawPolyline(geo.arc) + + // outer shell boundary (Vesicle), drawn lighter as a reference. + if (geo.shellArc) { + ctx.lineWidth = 1 + ctx.strokeStyle = root.dimColor + drawPolyline(geo.shellArc) + } + + for (let k = 0; k < geo.vecs.length; ++k) { + const v = geo.vecs[k] + const a = S(v.from) + const b = S(v.to) + const baseS = S(v.base) + const freeS = S(v.free) + + // arrowhead geometry at the mext end (b); size scales with shaft + const shaftLen = Math.hypot(b.x - a.x, b.y - a.y) + const headLen = Math.max(8, Math.min(shaftLen * 0.3, 18)) + const ang = Math.atan2(b.y - a.y, b.x - a.x) + const bx = b.x - headLen * Math.cos(ang) + const by = b.y - headLen * Math.sin(ang) + + // shaft (stop at the triangle base so it doesn't poke through) + ctx.lineWidth = 3 + ctx.strokeStyle = root.vectorColor + ctx.beginPath() + ctx.moveTo(a.x, a.y) + ctx.lineTo(bx, by) + ctx.stroke() + + // filled triangle arrowhead + ctx.fillStyle = root.vectorColor + ctx.beginPath() + ctx.moveTo(b.x, b.y) + ctx.lineTo(b.x - headLen * Math.cos(ang - headAng), + b.y - headLen * Math.sin(ang - headAng)) + ctx.lineTo(b.x - headLen * Math.cos(ang + headAng), + b.y - headLen * Math.sin(ang + headAng)) + ctx.closePath() + ctx.fill() + + // dot marks the end tied to the baseline + ctx.beginPath() + ctx.arc(baseS.x, baseS.y, 3.5, 0, 2 * Math.PI) + ctx.fill() + + // component name centred on the vector, parallel to it, on a + // translucent white plate. + if (root.showComponentNames) { + const mx = (baseS.x + freeS.x) / 2 + const my = (baseS.y + freeS.y) / 2 + const vAng = Math.atan2(freeS.y - baseS.y, freeS.x - baseS.x) + drawLabel(mx, my, vAng, root._compName(v.compIndex), + compFontPx, Qt.rgba(1, 1, 1, 0.5), root.labelColor) + } + } + } + + // ---- Blueprint-style dimension helpers ---- + const dimFont = Math.max(10, fontPx * 0.8) + function dimHead(tx, ty, ang) { + const hl = 7, ha = Math.PI / 7 + ctx.beginPath() + ctx.moveTo(tx, ty) + ctx.lineTo(tx - hl * Math.cos(ang - ha), ty - hl * Math.sin(ang - ha)) + ctx.lineTo(tx - hl * Math.cos(ang + ha), ty - hl * Math.sin(ang + ha)) + ctx.closePath() + ctx.fill() + } + // ---- Draw a ring's dmin + rmin dimensions ---- + function drawDims(geo) { + ctx.strokeStyle = root.dimColor + ctx.fillStyle = root.dimColor + ctx.lineWidth = 1 + ctx.font = dimFont + "px sans-serif" + ctx.textAlign = "center" + ctx.textBaseline = "middle" + + // dmin: between the baseline ends of the first two vectors, + // offset toward the centre (the clear zone). Needs both vectors + // to survive the cut-off. + if (geo.vecs.length >= 2) { + const q1 = S(geo.vecs[0].base) + const q2 = S(geo.vecs[1].base) + const midD = Qt.point((q1.x + q2.x) / 2, (q1.y + q2.y) / 2) + let inx = centerS.x - midD.x, iny = centerS.y - midD.y + const inl = Math.max(Math.hypot(inx, iny), 1e-3) + inx /= inl; iny /= inl + const dOff = 26 + const e1 = Qt.point(q1.x + inx * dOff, q1.y + iny * dOff) + const e2 = Qt.point(q2.x + inx * dOff, q2.y + iny * dOff) + ctx.beginPath() + ctx.moveTo(q1.x, q1.y); ctx.lineTo(e1.x + inx * 4, e1.y + iny * 4) + ctx.moveTo(q2.x, q2.y); ctx.lineTo(e2.x + inx * 4, e2.y + iny * 4) + ctx.stroke() + ctx.beginPath() + ctx.moveTo(e1.x, e1.y); ctx.lineTo(e2.x, e2.y) + ctx.stroke() + const dAng = Math.atan2(e2.y - e1.y, e2.x - e1.x) + dimHead(e1.x, e1.y, dAng + Math.PI) + dimHead(e2.x, e2.y, dAng) + drawLabel((e1.x + e2.x) / 2, (e1.y + e2.y) / 2, dAng, + "dmin " + geo.d.toFixed(2) + " nm", + dimFont, root.clearColor, root.dimColor) + } + + // rmin: short radius leader pointing along the radius, arrowhead + // at the arc, label running parallel along the leader. + if (geo.showRmin) { + const arcMidS = S(geo.arcMid) + const rAng = Math.atan2(arcMidS.y - centerS.y, arcMidS.x - centerS.x) + const leaderPx = root.radiusLeaderNm * scale + const rStart = Qt.point(arcMidS.x - Math.cos(rAng) * leaderPx, + arcMidS.y - Math.sin(rAng) * leaderPx) + ctx.beginPath() + ctx.moveTo(rStart.x, rStart.y) + ctx.lineTo(arcMidS.x, arcMidS.y) + ctx.stroke() + dimHead(arcMidS.x, arcMidS.y, rAng) + drawLabel((rStart.x + arcMidS.x) / 2, (rStart.y + arcMidS.y) / 2, rAng, + "R " + geo.r.toFixed(2) + " nm", + dimFont, root.clearColor, root.dimColor) + } + + // shell thickness (Vesicle): full radial dimension from the inner + // baseline out to the outer shell, with arrowheads at both ends. + if (geo.shell > 0 && geo.shellInner && geo.shellOuter) { + const si = S(geo.shellInner) + const so = S(geo.shellOuter) + const sAng = Math.atan2(so.y - si.y, so.x - si.x) + ctx.beginPath() + ctx.moveTo(si.x, si.y); ctx.lineTo(so.x, so.y) + ctx.stroke() + dimHead(si.x, si.y, sAng + Math.PI) + dimHead(so.x, so.y, sAng) + drawLabel((si.x + so.x) / 2, (si.y + so.y) / 2, sAng, + "shell " + geo.shell.toFixed(2) + " nm", + dimFont, root.clearColor, root.dimColor) + } + } + + for (let g = 0; g < geos.length; ++g) + drawRing(geos[g]) + if (root.showDimensions) + for (let g2 = 0; g2 < geos.length; ++g2) + drawDims(geos[g2]) + } + } + + // Empty state. + EaElements.Label { + anchors.centerIn: parent + visible: !root.components || root.components.count === 0 + color: EaStyle.Colors.themeForegroundMinor + text: qsTr("No components yet — load or create one in the sidebar.") + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/RodShape.qml b/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/RodShape.qml new file mode 100644 index 0000000..ea7d8be --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/RodShape.qml @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick + +import EasyApplication.Gui.Style as EaStyle + + +// Schematic of the rod body for the Rod structure's Shape view. The rod is a +// capsule: two half-sphere end caps joined by a cylindrical body. The body is +// filled with evenly spaced ribs, one per turn (so the rod grows with `turns`). +// A "turns N" dimension is drawn underneath. +Canvas { + id: rod + + property int turns: 1 + property bool showDimensions: false + + property color strokeColor: EaStyle.Colors.themeForegroundDisabled + property color dimColor: EaStyle.Colors.themeForegroundMinor + property color clearColor: EaStyle.Colors.mainContentBackground + + onTurnsChanged: requestPaint() + onShowDimensionsChanged: requestPaint() + onStrokeColorChanged: requestPaint() + onDimColorChanged: requestPaint() + onClearColorChanged: requestPaint() + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + + onPaint: { + const ctx = getContext("2d") + ctx.reset() + ctx.clearRect(0, 0, width, height) + + const n = Math.max(1, rod.turns) + + // The rod keeps real proportions (length grows with turns, fixed radius) + // and is scaled uniformly to fit a target width. So more turns -> longer + // rod -> scaled down -> visibly thinner (height shrinks), which reads as a + // more realistic rod. + const leftPad = 18 // left-aligned with padding + let capR = Math.min(24, height * 0.22) + const perTurn = 3 // body length per turn before scaling + let bodyW = n * perTurn + let totalW = bodyW + 2 * capR + const avail = width * 0.6 // narrower target so the rod isn't too wide + if (totalW > avail) { + const s = avail / totalW + capR *= s; bodyW *= s; totalW *= s // uniform scale: width and height shrink together + } + + const cy = height * 0.62 // sit lower in the view; leave room for the dimension below + const startX = leftPad + const bodyL = startX + capR + const bodyR = bodyL + bodyW + + // ---- Capsule outline ---- + ctx.lineWidth = 2 + ctx.strokeStyle = rod.strokeColor + + // top & bottom edges of the cylindrical body + ctx.beginPath() + ctx.moveTo(bodyL, cy - capR); ctx.lineTo(bodyR, cy - capR) + ctx.moveTo(bodyL, cy + capR); ctx.lineTo(bodyR, cy + capR) + ctx.stroke() + + // left half-sphere cap (bulges left), right cap (bulges right) + ctx.beginPath() + ctx.arc(bodyL, cy, capR, Math.PI / 2, Math.PI * 3 / 2) + ctx.stroke() + ctx.beginPath() + ctx.arc(bodyR, cy, capR, -Math.PI / 2, Math.PI / 2) + ctx.stroke() + + // ---- Turn ribs (one vertical line per turn, small spacing) ---- + ctx.lineWidth = 1 + const ribStep = bodyW / n + for (let i = 0; i < n; ++i) { + const x = bodyL + (i + 0.5) * ribStep + ctx.beginPath() + ctx.moveTo(x, cy - capR); ctx.lineTo(x, cy + capR) + ctx.stroke() + } + + // ---- "turns N" dimension underneath (gated by the Dimensions toggle) ---- + if (rod.showDimensions) { + const dy = cy + capR + 18 + ctx.strokeStyle = rod.dimColor + ctx.fillStyle = rod.dimColor + ctx.lineWidth = 1 + // witness lines down from the body ends (between the half-spheres only) + ctx.beginPath() + ctx.moveTo(bodyL, cy + capR + 4); ctx.lineTo(bodyL, dy + 4) + ctx.moveTo(bodyR, cy + capR + 4); ctx.lineTo(bodyR, dy + 4) + // dimension line + ctx.moveTo(bodyL, dy); ctx.lineTo(bodyR, dy) + ctx.stroke() + + function dimHead(tx, ty, ang) { + const hl = 7, ha = Math.PI / 7 + ctx.beginPath() + ctx.moveTo(tx, ty) + ctx.lineTo(tx - hl * Math.cos(ang - ha), ty - hl * Math.sin(ang - ha)) + ctx.lineTo(tx - hl * Math.cos(ang + ha), ty - hl * Math.sin(ang + ha)) + ctx.closePath() + ctx.fill() + } + dimHead(bodyL, dy, Math.PI) + dimHead(bodyR, dy, 0) + + // label with a cleared gap punched behind it + const px = Math.max(10, EaStyle.Sizes.fontPixelSize * 0.8) + ctx.font = px + "px sans-serif" + ctx.textAlign = "center" + ctx.textBaseline = "middle" + const text = qsTr("turns %1").arg(n) + const w = ctx.measureText(text).width + 6 + const lx = (bodyL + bodyR) / 2 + ctx.fillStyle = rod.clearColor + ctx.fillRect(lx - w / 2, dy - px * 0.75, w, px * 1.5) + ctx.fillStyle = rod.dimColor + ctx.fillText(text, lx, dy) + } + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/Shape.qml b/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/Shape.qml new file mode 100644 index 0000000..945d9bc --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/MainArea/Shape.qml @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements + +import Gui.Globals as Globals + + +// Main-area window: "Shape". A simple geometric schematic of the sample model, +// focused on the per-component mint-mext vectors and their spacing (dmin) along +// the structure baseline. Always available. +// +// The schematic depends on the selected structure type: +// - Ring: a single ring (dmin / rmin / rev). +// - Ball: one concentric ring per layer, alternating orientation. +// - Vesicle: one bilayer (two leaflet rings) per lamella, plus outer shell. +// - Rod: a single ring (like Ring) plus a rod-body capsule in the lower left. +// - Bilayer / Monolayer: two flat (uncurved) leaflets separated by zsep, drawn +// by FlatShapeView. Monolayer reverses the mint->mext direction. +// Other types fall back to a placeholder. +Rectangle { + id: root + + readonly property string structureType: Globals.BackendWrapper.sampleModelCurrentStructureType + readonly property bool isRod: structureType === "Rod" + readonly property bool isFlat: structureType === "Bilayer" || structureType === "Monolayer" + readonly property bool isRadial: structureType === "Ring" + || structureType === "Ball" + || structureType === "Vesicle" + || isRod + readonly property bool supported: isRadial || isFlat + + color: EaStyle.Colors.mainContentBackground + + RingShapeView { + id: shapeView + anchors.fill: parent + visible: root.isRadial + + showComponentNames: namesCheck.checked + showDimensions: dimsCheck.checked + + // Single-ring parameters (Ring, and Rod which reuses the ring layout). + dmin: root.isRod + ? Globals.BackendWrapper.rodStructure.dmin + : Globals.BackendWrapper.ringStructure.dmin + rmin: root.isRod + ? Globals.BackendWrapper.rodStructure.rmin + : Globals.BackendWrapper.ringStructure.rmin + + // Each structure has its own rev flag. + rev: root.structureType === "Ball" + ? Globals.BackendWrapper.ballStructure.rev + : root.structureType === "Vesicle" + ? Globals.BackendWrapper.vesicleStructure.rev + : root.isRod + ? Globals.BackendWrapper.rodStructure.rev + : Globals.BackendWrapper.ringStructure.rev + + // Ball: one ring per layer. Ring/Rod: no layers -> single ring above. + layers: root.structureType === "Ball" + ? Globals.BackendWrapper.layersItems + : null + + // Vesicle: one bilayer per lamella. + lamellae: root.structureType === "Vesicle" + ? Globals.BackendWrapper.lamellaeItems + : null + } + + // Bilayer / Monolayer: flat leaflets. + FlatShapeView { + anchors.fill: parent + visible: root.isFlat + + showComponentNames: namesCheck.checked + showDimensions: dimsCheck.checked + + // Monolayer is the opposite of Bilayer. + reversed: root.structureType === "Monolayer" + + dmin: root.structureType === "Monolayer" + ? Globals.BackendWrapper.monolayerStructure.dmin + : Globals.BackendWrapper.bilayerStructure.dmin + zsep: root.structureType === "Monolayer" + ? Globals.BackendWrapper.monolayerStructure.zsep + : Globals.BackendWrapper.bilayerStructure.zsep + nside: root.structureType === "Monolayer" + ? Globals.BackendWrapper.monolayerStructure.nside + : Globals.BackendWrapper.bilayerStructure.nside + } + + // Rod body capsule, drawn in the lower-left quadrant for the Rod structure. + RodShape { + anchors.left: parent.left + anchors.bottom: parent.bottom + width: parent.width * 0.45 + height: parent.height * 0.45 + visible: root.isRod + turns: Globals.BackendWrapper.rodStructure.turns + showDimensions: dimsCheck.checked + } + + // Display toggles (top-right), available for every supported view. + Row { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: EaStyle.Sizes.fontPixelSize + spacing: EaStyle.Sizes.fontPixelSize + visible: root.supported + + EaElements.CheckBox { + id: namesCheck + text: qsTr("Component names") + checked: false + } + EaElements.CheckBox { + id: dimsCheck + text: qsTr("Dimensions") + checked: false + } + } + + // Placeholder for structure types whose Shape view is not implemented yet. + Column { + anchors.centerIn: parent + spacing: EaStyle.Sizes.fontPixelSize + visible: !root.supported + + EaElements.Label { + anchors.horizontalCenter: parent.horizontalCenter + font.pixelSize: EaStyle.Sizes.fontPixelSize * 1.5 + color: EaStyle.Colors.themeForegroundMinor + text: qsTr("Shape") + } + EaElements.Label { + anchors.horizontalCenter: parent.horizontalCenter + color: EaStyle.Colors.themeForegroundMinor + text: qsTr("No Shape view yet for the %1 structure.").arg(root.structureType) + } + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Groups/ComponentsFiles.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Groups/ComponentsFiles.qml new file mode 100644 index 0000000..09bbed0 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Groups/ComponentsFiles.qml @@ -0,0 +1,227 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents + +import Gui.Globals as Globals + + +EaElements.GroupColumn { + id: root + property double thirdWidth: (EaStyle.Sizes.sideBarContentWidth - 2 * EaStyle.Sizes.fontPixelSize) / 3 + + // Row of the file picked in the list, -1 when nothing is selected. Drives + // whether Export writes one file or the whole component. + readonly property int selectedFileRow: componentFilesList.selectedIndexes.length > 0 + ? componentFilesList.selectedIndexes[0].row + : -1 + + readonly property int fileCount: Globals.BackendWrapper.componentsFilesFiles + ? Globals.BackendWrapper.componentsFilesFiles.count + : 0 + + // Component selector + name editor + Create new, in one row of equal + // thirds. The dropdown lists only components already loaded on the Basic + // tab; picking one loads its files and name. + Row { + spacing: EaStyle.Sizes.fontPixelSize + + Column { + EaElements.Label { + enabled: false + text: qsTr("Component") + } + EaElements.ComboBox { + id: componentSelector + width: root.thirdWidth + textRole: "name" + model: Globals.BackendWrapper.componentsLoaded + + onActivated: (i) => { + const row = Globals.BackendWrapper.componentsLoaded.get(i) + if (row) Globals.BackendWrapper.componentsFilesSelect(row.name) + } + + // Mirror the dropdown index from the backend's selected + // component (set by picking a row, Create new, or Save to lib). + function syncIndex() { + const sel = Globals.BackendWrapper.componentsFilesSelectedComponent + const m = Globals.BackendWrapper.componentsLoaded + if (sel !== "" && m) { + for (let i = 0; i < m.count; ++i) { + if (m.get(i).name === sel) { + currentIndex = i + return + } + } + } + currentIndex = -1 + } + + Component.onCompleted: syncIndex() + Connections { + target: Globals.BackendWrapper.componentsLoaded + function onCountChanged() { componentSelector.syncIndex() } + } + Connections { + target: Globals.BackendWrapper + function onComponentsFilesSelectedComponentChanged() { componentSelector.syncIndex() } + } + } + } + + Column { + EaElements.Label { + enabled: false + text: qsTr("Name") + } + EaElements.TextField { + width: root.thirdWidth + horizontalAlignment: TextInput.AlignLeft + placeholderText: qsTr("Component name") + text: Globals.BackendWrapper.componentsFilesEditName + onEditingFinished: Globals.BackendWrapper.componentsFilesSetEditName(text) + } + } + + Column { + // Empty spacer label so the button bottom-aligns with the fields. + EaElements.Label { + enabled: false + text: " " + } + EaElements.SideBarButton { + fontIcon: "plus-square" + text: qsTr("Create new") + width: root.thirdWidth + ToolTip.text: qsTr("Start a new component — clears the selection, name and file list") + onClicked: Globals.BackendWrapper.componentsFilesCreateNew() + } + } + } + + EaComponents.ListView { + id: componentFilesList + defaultInfoText: qsTr("No files associated with this component") + multiSelection: false + + columnWidths: [ + EaStyle.Sizes.fontPixelSize * 2.5, + -1, + EaStyle.Sizes.fontPixelSize * 5, + EaStyle.Sizes.tableRowHeight, + EaStyle.Sizes.tableRowHeight + ] + + header: EaComponents.ListViewHeader { + EaComponents.TableViewLabel { + text: qsTr("№") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Path") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Size") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + color: EaStyle.Colors.themeForegroundMinor + } + } + + model: Globals.BackendWrapper.componentsFilesFiles + + delegateModelAccess: DelegateModel.ReadWrite + + delegate: EaComponents.ListViewDelegate { + required property int index + required property string path + required property string size + + EaComponents.TableViewLabel { + text: index + 1 + enabled: false + } + EaComponents.TableViewLabel { + text: path + elide: Text.ElideLeft + } + EaComponents.TableViewLabel { + text: size + enabled: false + } + EaComponents.TableViewButton { + fontIcon: "edit" + ToolTip.text: qsTr("Edit this file") + onClicked: Globals.BackendWrapper.componentsFilesEditFile(index) + } + EaComponents.TableViewButton { + fontIcon: "minus-circle" + ToolTip.text: qsTr("Remove this file") + onClicked: Globals.BackendWrapper.componentsFilesRemove(index) + } + } + } + + Grid { + columns: 3 + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.SideBarButton { + fontIcon: "plus-circle" + text: qsTr("Add new file(s)") + width: root.thirdWidth + ToolTip.text: qsTr("Pick one or more files from disk and add them to this component") + onClicked: addFilesLoader.item.open() + } + + EaElements.SideBarButton { + fontIcon: "save" + text: qsTr("Save to lib") + width: root.thirdWidth + ToolTip.text: qsTr("Save this component to the asset library and load it on the Basic tab") + enabled: Globals.BackendWrapper.componentsFilesEditName.trim() !== "" + onClicked: Globals.BackendWrapper.componentsFilesSave() + } + + EaElements.SideBarButton { + fontIcon: "file-export" + text: qsTr("Export") + width: root.thirdWidth + ToolTip.text: qsTr("Export the selected file to disk, or the whole component directory when no file is selected") + enabled: root.fileCount > 0 + onClicked: exportDestinationDialog.open() + } + } + + Loader { + id: addFilesLoader + source: "../Popups/AddComponentFiles.qml" + } + + // Destination picker for Export. What gets written is decided here rather + // than on the button, so the selection is read when the user confirms. + FolderDialog { + id: exportDestinationDialog + title: qsTr("Choose a destination directory") + onAccepted: { + const destination = selectedFolder.toString() + if (root.selectedFileRow >= 0) + Globals.BackendWrapper.componentsFilesExportFile(root.selectedFileRow, destination) + else + Globals.BackendWrapper.componentsFilesExportComponent(destination) + } + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Groups/LibraryAssetsFiles.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Groups/LibraryAssetsFiles.qml new file mode 100644 index 0000000..38c7355 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Groups/LibraryAssetsFiles.qml @@ -0,0 +1,400 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents + +import Gui.Globals as Globals + + +EaElements.GroupColumn { + id: root + property double halfWidth: (EaStyle.Sizes.sideBarContentWidth - EaStyle.Sizes.fontPixelSize) / 2 + property double thirdWidth: (EaStyle.Sizes.sideBarContentWidth - 2 * EaStyle.Sizes.fontPixelSize) / 3 + + readonly property string assetType: Globals.BackendWrapper.libraryAssetsType + readonly property bool isSalt: assetType === "Salt" + + readonly property int fileCount: Globals.BackendWrapper.libraryAssetsPaths + ? Globals.BackendWrapper.libraryAssetsPaths.count + : 0 + + // Row of the file picked in the list, -1 when nothing is selected (or the + // list is hidden for a salt). Drives whether Export writes one file or the + // whole asset. + readonly property int selectedFileRow: !isSalt && assetPathsList.selectedIndexes.length > 0 + ? assetPathsList.selectedIndexes[0].row + : -1 + + // Component-only fields (C-ion/Mint/Mext) show for the molecule types and + // hide for Ion/Solvent/Salt, mirroring the asset class hierarchy. + readonly property bool isMolecule: assetType === "Lipid" + || assetType === "Surfactant" + || assetType === "Component (Other)" + + // Ion options: a leading "(None)" sentinel (empty value) plus the shared + // ion library. Used by both the C-ion picker and the salt composition. + readonly property var ionOptions: { + const lib = Globals.BackendWrapper.ionsAvailable + let opts = [qsTr("(None)")] + for (let i = 0; i < lib.count; ++i) + opts.push(lib.get(i).name) + return opts + } + + // Row 1 — Create new / Load from lib. + Grid { + columns: 2 + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.SideBarButton { + fontIcon: "plus-square" + text: qsTr("Create new") + width: root.halfWidth + ToolTip.text: qsTr("Start a new library asset — clears the fields and unlocks the type") + onClicked: Globals.BackendWrapper.libraryAssetsCreateNew() + } + + EaElements.SideBarButton { + fontIcon: "file-import" + text: qsTr("Load from lib") + width: root.halfWidth + ToolTip.text: qsTr("Browse and load an existing asset from the library") + onClicked: loadLibraryAssetLoader.item.open() + } + } + + // Row 2 — Type + Name. Type is editable only while creating. + Row { + spacing: EaStyle.Sizes.fontPixelSize + + Column { + EaElements.Label { + enabled: false + text: qsTr("Type") + } + EaElements.ComboBox { + id: typeSelector + width: root.halfWidth + model: Globals.BackendWrapper.libraryAssetsTypeOptions + enabled: Globals.BackendWrapper.libraryAssetsMode === "create" + displayText: currentIndex < 0 ? "" : currentText + + function syncIndex() { + currentIndex = model + ? model.indexOf(Globals.BackendWrapper.libraryAssetsType) + : -1 + } + Component.onCompleted: syncIndex() + onActivated: (i) => Globals.BackendWrapper.libraryAssetsSetType(model[i]) + Connections { + target: Globals.BackendWrapper + function onLibraryAssetsTypeChanged() { typeSelector.syncIndex() } + } + } + } + + Column { + EaElements.Label { + enabled: false + text: qsTr("Name") + } + EaElements.TextField { + width: root.halfWidth + horizontalAlignment: TextInput.AlignLeft + placeholderText: qsTr("Asset name") + text: Globals.BackendWrapper.libraryAssetsName + onEditingFinished: Globals.BackendWrapper.libraryAssetsSetName(text) + } + } + } + + // Row 3 — component-only fields (C-ion / Mint / Mext). + Row { + visible: root.isMolecule + spacing: EaStyle.Sizes.fontPixelSize + + Column { + EaElements.Label { + enabled: false + text: qsTr("C-ion") + } + EaElements.ComboBox { + id: cIonSelector + width: root.thirdWidth + model: root.ionOptions + // Blank collapsed label for the "(None)" sentinel (index 0) or + // no match (-1); the dropdown still lists "(None)". + displayText: currentIndex <= 0 ? "" : currentText + + function syncIndex() { + const v = Globals.BackendWrapper.libraryAssetsCIon + currentIndex = v === "" ? 0 : find(v) + } + Component.onCompleted: syncIndex() + onActivated: (i) => Globals.BackendWrapper.libraryAssetsSetCIon(i === 0 ? "" : model[i]) + Connections { + target: Globals.BackendWrapper + function onLibraryAssetsCIonChanged() { cIonSelector.syncIndex() } + } + } + } + + Column { + EaElements.Label { + enabled: false + text: qsTr("Mint") + } + EaElements.TextField { + width: root.thirdWidth + horizontalAlignment: TextInput.AlignLeft + validator: IntValidator { bottom: 0 } + text: Globals.BackendWrapper.libraryAssetsMint + onEditingFinished: Globals.BackendWrapper.libraryAssetsSetMint(parseInt(text) || 0) + } + } + + Column { + EaElements.Label { + enabled: false + text: qsTr("Mext") + } + EaElements.TextField { + width: root.thirdWidth + horizontalAlignment: TextInput.AlignLeft + validator: IntValidator { bottom: 0 } + text: Globals.BackendWrapper.libraryAssetsMext + onEditingFinished: Globals.BackendWrapper.libraryAssetsSetMext(parseInt(text) || 0) + } + } + } + + // Salt composition — shown only for the Salt type (replaces the file list). + // Two ion slots with stoichiometric counts. + Column { + visible: root.isSalt + width: parent.width + + EaElements.Label { + enabled: false + text: qsTr("Salt composition") + } + + EaComponents.ListView { + id: saltComposition + multiSelection: false + + columnWidths: [ + EaStyle.Sizes.fontPixelSize * 2.5, + EaStyle.Sizes.fontPixelSize * 7, + EaStyle.Sizes.fontPixelSize * 6, + EaStyle.Sizes.tableColumnFlex + ] + + header: EaComponents.ListViewHeader { + EaComponents.TableViewLabel { + text: qsTr("№") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Name") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Count") + color: EaStyle.Colors.themeForegroundMinor + horizontalAlignment: Text.AlignHCenter + } + EaComponents.TableViewLabel {} // filler, takes the remaining space + } + + model: Globals.BackendWrapper.libraryAssetsSaltComposition + + delegate: EaComponents.ListViewDelegate { + id: saltRow + required property int index + required property string ion + required property int count + + EaComponents.TableViewLabel { + text: index + 1 + enabled: false + } + + EaComponents.TableViewComboBox { + id: ionCombo + model: root.ionOptions + displayText: currentIndex <= 0 ? "" : currentText + + function sync() { currentIndex = saltRow.ion === "" ? 0 : find(saltRow.ion) } + Component.onCompleted: sync() + onActivated: (i) => Globals.BackendWrapper.libraryAssetsSetSaltIon(saltRow.index, i === 0 ? "" : model[i]) + Connections { + target: saltRow + function onIonChanged() { ionCombo.sync() } + } + } + + EaComponents.ListViewTextInput { + text: count + validator: IntValidator { bottom: 1 } + onEditingFinished: Globals.BackendWrapper.libraryAssetsSetSaltCount(saltRow.index, parseInt(text) || 1) + } + + EaComponents.TableViewLabel {} // filler, matches the header + } + } + } + + // Asset composition — the files making up this asset. Hidden for salts. + EaComponents.ListView { + id: assetPathsList + visible: !root.isSalt + defaultInfoText: qsTr("No files in this asset") + multiSelection: false + + columnWidths: [ + EaStyle.Sizes.fontPixelSize * 2.5, + -1, + EaStyle.Sizes.fontPixelSize * 5, + EaStyle.Sizes.tableRowHeight, + EaStyle.Sizes.tableRowHeight + ] + + header: EaComponents.ListViewHeader { + EaComponents.TableViewLabel { + text: qsTr("№") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Path") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Size") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + color: EaStyle.Colors.themeForegroundMinor + } + } + + model: Globals.BackendWrapper.libraryAssetsPaths + + delegateModelAccess: DelegateModel.ReadWrite + + delegate: EaComponents.ListViewDelegate { + required property int index + required property string path + required property string size + + EaComponents.TableViewLabel { + text: index + 1 + enabled: false + } + EaComponents.TableViewLabel { + text: path + elide: Text.ElideLeft + } + EaComponents.TableViewLabel { + text: size + enabled: false + } + EaComponents.TableViewButton { + fontIcon: "edit" + ToolTip.text: qsTr("Edit this file") + onClicked: Globals.BackendWrapper.libraryAssetsEditPath(index) + } + EaComponents.TableViewButton { + fontIcon: "minus-circle" + ToolTip.text: qsTr("Remove this file") + onClicked: Globals.BackendWrapper.libraryAssetsRemovePath(index) + } + } + } + + // Row — Add new file / Save to lib / Export. + Grid { + columns: 3 + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.SideBarButton { + fontIcon: "plus-circle" + text: qsTr("Add new file") + width: root.thirdWidth + ToolTip.text: qsTr("Pick one or more files from disk and add them to this asset") + enabled: Globals.BackendWrapper.libraryAssetsMode !== "empty" && !root.isSalt + onClicked: addAssetFilesDialog.open() + } + + EaElements.SideBarButton { + fontIcon: "save" + text: qsTr("Save to lib") + width: root.thirdWidth + ToolTip.text: qsTr("Save this asset to the library") + enabled: root.isSalt + ? Globals.BackendWrapper.libraryAssetsSaltReady + : (Globals.BackendWrapper.libraryAssetsName.trim() !== "" + && Globals.BackendWrapper.libraryAssetsPaths + && Globals.BackendWrapper.libraryAssetsPaths.count > 0) + onClicked: Globals.BackendWrapper.libraryAssetsSave() + } + + EaElements.SideBarButton { + fontIcon: "file-export" + text: qsTr("Export") + width: root.thirdWidth + ToolTip.text: qsTr("Export the selected file to disk, or the whole asset directory when no file is selected") + // A salt carries no files, so it falls back to the composition + // readiness check used by Save. + enabled: root.isSalt + ? Globals.BackendWrapper.libraryAssetsSaltReady + : root.fileCount > 0 + onClicked: exportDestinationDialog.open() + } + } + + // Destination picker for Export. What gets written is decided here rather + // than on the button, so the selection is read when the user confirms. + FolderDialog { + id: exportDestinationDialog + title: qsTr("Choose a destination directory") + onAccepted: { + const destination = selectedFolder.toString() + if (root.selectedFileRow >= 0) + Globals.BackendWrapper.libraryAssetsExportPath(root.selectedFileRow, destination) + else + Globals.BackendWrapper.libraryAssetsExport(destination) + } + } + + FileDialog { + id: addAssetFilesDialog + fileMode: FileDialog.OpenFiles + nameFilters: [ + "Any (*)", + "Structure files (*.gro *.pdb *.xyz)", + "Topology files (*.itp *.top)", + "Smiles files (*.sml)" + ] + onAccepted: { + for (let i = 0; i < selectedFiles.length; ++i) { + Globals.BackendWrapper.libraryAssetsAppendPath(selectedFiles[i].toString()) + } + } + } + + Loader { + id: loadLibraryAssetLoader + source: "../Popups/LoadLibraryAsset.qml" + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Groups/SmilesGenerator.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Groups/SmilesGenerator.qml new file mode 100644 index 0000000..915ce87 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Groups/SmilesGenerator.qml @@ -0,0 +1,210 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements + +import Gui.Globals as Globals + + +EaElements.GroupColumn { + id: root + property double halfWidth: (EaStyle.Sizes.sideBarContentWidth - EaStyle.Sizes.fontPixelSize) / 2 + property double thirdWidth: (EaStyle.Sizes.sideBarContentWidth - 2 * EaStyle.Sizes.fontPixelSize) / 3 + + // Map the display type to the value stored on a component ('Other' rather + // than 'Component (Other)', matching the components list). + function storedComponentType(label) { + return label === "Component (Other)" ? "Other" : label + } + + // Molecule identity — Name and Formula. + Row { + spacing: EaStyle.Sizes.fontPixelSize + + Column { + EaElements.Label { + enabled: false + text: qsTr("Name") + } + EaElements.TextField { + width: root.halfWidth + horizontalAlignment: TextInput.AlignLeft + placeholderText: qsTr("e.g. SDS") + text: Globals.BackendWrapper.smilesMoleculeName + onEditingFinished: Globals.BackendWrapper.smilesSetMoleculeName(text) + } + } + + Column { + EaElements.Label { + enabled: false + text: qsTr("Formula") + } + EaElements.TextField { + width: root.halfWidth + horizontalAlignment: TextInput.AlignLeft + placeholderText: qsTr("(optional)") + text: Globals.BackendWrapper.smilesFormula + onEditingFinished: Globals.BackendWrapper.smilesSetFormula(text) + } + } + } + + // The SMILES string. Hydrogens are added automatically by the generator. + Column { + width: parent.width + + EaElements.Label { + enabled: false + text: qsTr("SMILES") + } + EaElements.TextField { + width: EaStyle.Sizes.sideBarContentWidth + horizontalAlignment: TextInput.AlignLeft + placeholderText: qsTr("e.g. CCCCCCCCCCCCOS(=O)(=O)[O-]") + text: Globals.BackendWrapper.smilesString + onEditingFinished: Globals.BackendWrapper.smilesSetString(text) + } + EaElements.Label { + enabled: false + text: qsTr("Hydrogens are added automatically.") + } + } + + // Simulation box. + Column { + width: parent.width + + EaElements.Label { + enabled: false + text: qsTr("Box") + } + Row { + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.Parameter { + width: root.thirdWidth + title: qsTr("Lx") + units: "nm" + validator: DoubleValidator { bottom: 0 } + text: Globals.BackendWrapper.smilesBoxX + onEditingFinished: Globals.BackendWrapper.smilesSetBoxX(parseFloat(text) || 0) + } + EaElements.Parameter { + width: root.thirdWidth + title: qsTr("Ly") + units: "nm" + validator: DoubleValidator { bottom: 0 } + text: Globals.BackendWrapper.smilesBoxY + onEditingFinished: Globals.BackendWrapper.smilesSetBoxY(parseFloat(text) || 0) + } + EaElements.Parameter { + width: root.thirdWidth + title: qsTr("Lz") + units: "nm" + validator: DoubleValidator { bottom: 0 } + text: Globals.BackendWrapper.smilesBoxZ + onEditingFinished: Globals.BackendWrapper.smilesSetBoxZ(parseFloat(text) || 0) + } + } + } + + // Generation flags. Flatten-XZ only acts together with Align-to-Z, so it is + // disabled unless Align-to-Z is on. + Row { + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.CheckBox { + width: root.thirdWidth + height: EaStyle.Sizes.fontPixelSize * 2.5 + text: qsTr("Cis C=C") + ToolTip.text: qsTr("Kink C=C double bonds to cis (dbcis)") + checked: Globals.BackendWrapper.smilesCisDoubleBonds + onToggled: Globals.BackendWrapper.smilesSetCisDoubleBonds(checked) + } + EaElements.CheckBox { + width: root.thirdWidth + height: EaStyle.Sizes.fontPixelSize * 2.5 + text: qsTr("Align Z") + ToolTip.text: qsTr("Align the backbone to the Z axis") + checked: Globals.BackendWrapper.smilesAlignZ + onToggled: Globals.BackendWrapper.smilesSetAlignZ(checked) + } + EaElements.CheckBox { + width: root.thirdWidth + height: EaStyle.Sizes.fontPixelSize * 2.5 + text: qsTr("Flatten XZ") + ToolTip.text: qsTr("Flatten into the XZ plane (only with Align Z)") + enabled: Globals.BackendWrapper.smilesAlignZ + checked: Globals.BackendWrapper.smilesFlatXZ + onToggled: Globals.BackendWrapper.smilesSetFlatXZ(checked) + } + } + + // Component type + output format + Generate, in one row of thirds. + Row { + spacing: EaStyle.Sizes.fontPixelSize + + Column { + EaElements.Label { + enabled: false + text: qsTr("Type") + } + EaElements.ComboBox { + id: componentTypeSelector + width: root.thirdWidth + model: Globals.BackendWrapper.smilesComponentTypeOptions + Component.onCompleted: currentIndex = model.indexOf(Globals.BackendWrapper.smilesComponentType) + onActivated: (i) => Globals.BackendWrapper.smilesSetComponentType(model[i]) + } + } + + Column { + EaElements.Label { + enabled: false + text: qsTr("Format") + } + EaElements.ComboBox { + id: formatSelector + width: root.thirdWidth + model: Globals.BackendWrapper.smilesFormatOptions + Component.onCompleted: currentIndex = model.indexOf(Globals.BackendWrapper.smilesFormat) + onActivated: (i) => Globals.BackendWrapper.smilesSetFormat(model[i]) + } + } + + Column { + // Empty spacer label so the button bottom-aligns with the dropdowns. + EaElements.Label { + enabled: false + text: " " + } + EaElements.SideBarButton { + fontIcon: "cogs" + text: qsTr("Generate") + width: root.thirdWidth + ToolTip.text: qsTr("Generate the molecule and add it to the components") + enabled: Globals.BackendWrapper.smilesReady + onClicked: { + Globals.BackendWrapper.componentsAppend({ + name: Globals.BackendWrapper.smilesMoleculeName, + component_type: root.storedComponentType(Globals.BackendWrapper.smilesComponentType), + c_ion: "", + mint: 0, + mext: 0 + }) + console.debug("SMILES generate → component", + Globals.BackendWrapper.smilesMoleculeName, + "dbcis=" + (Globals.BackendWrapper.smilesCisDoubleBonds ? "['C']" : "[]"), + "alignZ=" + Globals.BackendWrapper.smilesAlignZ, + "fxz=" + Globals.BackendWrapper.smilesFlatXZ) + } + } + } + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Groups/StructureFiles.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Groups/StructureFiles.qml new file mode 100644 index 0000000..131d508 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Groups/StructureFiles.qml @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents + +import Gui.Globals as Globals + + +EaElements.GroupColumn { + id: root + property double thirdWidth: (EaStyle.Sizes.sideBarContentWidth - 2 * EaStyle.Sizes.fontPixelSize) / 3 + + EaComponents.ListView { + id: structureFilesList + defaultInfoText: qsTr("No sample model files available") + multiSelection: false + + columnWidths: [ + EaStyle.Sizes.fontPixelSize * 2.5, + -1, + EaStyle.Sizes.fontPixelSize * 5, + EaStyle.Sizes.tableRowHeight + ] + + header: EaComponents.ListViewHeader { + EaComponents.TableViewLabel { + text: qsTr("№") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("File") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Size") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + color: EaStyle.Colors.themeForegroundMinor + } + } + + model: Globals.BackendWrapper.structureFilesFiles + + delegateModelAccess: DelegateModel.ReadWrite + + delegate: EaComponents.ListViewDelegate { + required property int index + required property string path + required property string size + + EaComponents.TableViewLabel { + text: index + 1 + enabled: false + } + EaComponents.TableViewLabel { + text: path + elide: Text.ElideLeft + } + EaComponents.TableViewLabel { + text: size + enabled: false + } + EaComponents.TableViewButton { + fontIcon: "minus-circle" + ToolTip.text: qsTr("Remove this file") + onClicked: Globals.BackendWrapper.structureFilesRemove(index) + } + } + } + + Grid { + columns: 3 + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.SideBarButton { + fontIcon: "plus-circle" + text: qsTr("Add new file(s)") + width: root.thirdWidth + ToolTip.text: qsTr("Pick one or more files from disk and add them to the sample model") + onClicked: addSampleModelFilesDialog.open() + } + + EaElements.SideBarButton { + fontIcon: "save" + text: qsTr("Save to lib") + width: root.thirdWidth + ToolTip.text: qsTr("Save the current sample model files to the asset library") + enabled: Globals.BackendWrapper.structureFilesFiles + ? Globals.BackendWrapper.structureFilesFiles.count > 0 + : false + onClicked: Globals.BackendWrapper.structureFilesSaveToLib() + } + + EaElements.SideBarButton { + fontIcon: "file-export" + text: qsTr("Export") + width: root.thirdWidth + ToolTip.text: qsTr("Export the current sample model files to disk") + enabled: Globals.BackendWrapper.structureFilesFiles + ? Globals.BackendWrapper.structureFilesFiles.count > 0 + : false + onClicked: exportDestinationDialog.open() + } + } + + FolderDialog { + id: exportDestinationDialog + title: qsTr("Choose a destination directory") + onAccepted: Globals.BackendWrapper.structureFilesExport(selectedFolder.toString()) + } + + FileDialog { + id: addSampleModelFilesDialog + fileMode: FileDialog.OpenFiles + nameFilters: [ + "Any (*)", + "Structure files (*.gro *.pdb *.xyz)", + "Topology files (*.itp *.top)", + "Data files (*.dat)" + ] + onAccepted: { + for (let i = 0; i < selectedFiles.length; ++i) { + Globals.BackendWrapper.structureFilesAppendPath(selectedFiles[i].toString()) + } + } + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Layout.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Layout.qml new file mode 100644 index 0000000..7da8188 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Layout.qml @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls + +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents + + +EaComponents.SideBarColumn { + + EaElements.GroupBox { + title: qsTr("Library assets") + icon: "book" + collapsed: false + + Loader { source: "Groups/LibraryAssetsFiles.qml" } + } + + EaElements.GroupBox { + title: qsTr("Components Files") + icon: "file-alt" + + Loader { source: "Groups/ComponentsFiles.qml" } + } + + EaElements.GroupBox { + title: qsTr("Sample Model Files") + icon: "folder-open" + + Loader { source: "Groups/StructureFiles.qml" } + } + + EaElements.GroupBox { + title: qsTr("SMILES generator") + icon: "atom" + + Loader { source: "Groups/SmilesGenerator.qml" } + } + +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Popups/AddComponentFiles.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Popups/AddComponentFiles.qml new file mode 100644 index 0000000..55759c1 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Popups/AddComponentFiles.qml @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Dialogs + +import Gui.Globals as Globals + + +FileDialog { + fileMode: FileDialog.OpenFiles + nameFilters: [ + "Any (*)", + "Structure files (*.gro *.pdb *.xyz)", + "Topology files (*.itp)", + "Smiles files (*.sml)" + ] + + onAccepted: { + for (let i = 0; i < selectedFiles.length; ++i) { + Globals.BackendWrapper.componentsFilesAppend(selectedFiles[i].toString()) + } + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Popups/LoadLibraryAsset.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Popups/LoadLibraryAsset.qml new file mode 100644 index 0000000..89d6c36 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Popups/LoadLibraryAsset.qml @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +import EasyApplication.Gui.Components as EaComponents +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Style as EaStyle + +import Gui.Globals as Globals + + +EaElements.Dialog { + id: libraryAssetLoadDialog + + property int inputFieldWidth: EaStyle.Sizes.fontPixelSize * 30 + + title: qsTr("Load an Asset from the Library") + standardButtons: Dialog.Ok | Dialog.Cancel + + onAccepted: { + var indexes = loadLibraryAssetListView.selectedIndexes + if (indexes.length > 0) { + var item = Globals.BackendWrapper.libraryAssetsLibrary.get(indexes[0].row) + Globals.BackendWrapper.libraryAssetsLoad(item) + } + loadLibraryAssetListView.clearSelection() + } + onRejected: { + loadLibraryAssetListView.clearSelection() + } + + Column { + EaElements.Label { + enabled: false + text: qsTr("Available in the Asset Library") + } + + EaComponents.ListView { + id: loadLibraryAssetListView + defaultInfoText: qsTr("No assets found") + multiSelection: false + + columnWidths: [ + EaStyle.Sizes.fontPixelSize * 2.5, + -1, + EaStyle.Sizes.fontPixelSize * 10 + ] + + header: EaComponents.ListViewHeader { + EaComponents.TableViewLabel { + text: qsTr("№") + color: EaStyle.Colors.themeForegroundMinor + horizontalAlignment: Text.AlignHCenter + } + EaComponents.TableViewLabel { + text: qsTr("Name") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Type") + color: EaStyle.Colors.themeForegroundMinor + } + } + + model: Globals.BackendWrapper.libraryAssetsLibrary + + delegate: EaComponents.ListViewDelegate { + required property int index + required property string name + required property string type + + EaComponents.TableViewLabel { + text: index + 1 + horizontalAlignment: Text.AlignHCenter + enabled: false + } + EaComponents.TableViewLabel { + text: name + } + EaComponents.TableViewLabel { + text: type + } + } + } + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Popups/ReplaceStructure.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Popups/ReplaceStructure.qml new file mode 100644 index 0000000..2b891ca --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Advanced/Popups/ReplaceStructure.qml @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Dialogs + + +FolderDialog { + title: qsTr("Select a directory to replace the structure with") +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Components/Fractions.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Components/Fractions.qml new file mode 100644 index 0000000..e539904 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Components/Fractions.qml @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Components as EaComponents + +import Gui.Globals as Globals + +EaComponents.ListView { + // Override to bind a per-row Fractions backend (e.g. layer/lamella). + // Defaults to the global Fractions set on the wrapper. + property var fractionsModel: Globals.BackendWrapper.fractionsModel + + defaultInfoText: qsTr("Missing components") + selectionActive: false + + columnWidths: [ + EaStyle.Sizes.fontPixelSize * 6, + -1, + EaStyle.Sizes.fontPixelSize * 8, + EaStyle.Sizes.fontPixelSize * 8 + ] + + header: EaComponents.ListViewHeader { + EaComponents.TableViewLabel { + text: qsTr("Present") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel {} + EaComponents.TableViewLabel { + text: qsTr("Component name") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Mole ratio") + color: EaStyle.Colors.themeForegroundMinor + } + } + + model: fractionsModel + + delegateModelAccess: DelegateModel.ReadWrite + + delegate: EaComponents.ListViewDelegate { + required property int index + required property string name + required property double fracs + required property bool present + + EaComponents.TableViewCheckBox { + checked: present + onToggled: present = checked + } + EaComponents.TableViewLabel {} + EaComponents.TableViewLabel { + text: name + enabled: false + } + EaComponents.ListViewTextInput { + text: present ? fracs : 0 + enabled: present + onEditingFinished: fracs = parseFloat(text) + validator: DoubleValidator { bottom: 0 } + } + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/BallStructure.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/BallStructure.qml new file mode 100644 index 0000000..3b327c8 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/BallStructure.qml @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements + +import Gui.Globals as Globals + +EaElements.GroupColumn { + + Column { + width: EaStyle.Sizes.sideBarContentWidth * 0.2 + + EaElements.Label { + enabled: false + text: qsTr("Fill") + } + EaElements.ComboBox { + width: parent.width + model: ["FIBO", "RINGS", "RINGS0"] + Component.onCompleted: currentIndex = model.indexOf(Globals.BackendWrapper.ballStructure.fill) + onActivated: (i) => Globals.BackendWrapper.ballStructure.fill = model[i] + } + } + + Row { + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.CheckBox { + width: EaStyle.Sizes.sideBarContentWidth * 0.15 + height: EaStyle.Sizes.fontPixelSize * 3 + text: qsTr("Fxz") + checked: Globals.BackendWrapper.ballStructure.fxz + onToggled: Globals.BackendWrapper.ballStructure.fxz = checked + } + EaElements.CheckBox { + width: EaStyle.Sizes.sideBarContentWidth * 0.15 + height: EaStyle.Sizes.fontPixelSize * 3 + text: qsTr("Rev") + checked: Globals.BackendWrapper.ballStructure.rev + onToggled: Globals.BackendWrapper.ballStructure.rev = checked + } + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/BilayerStructure.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/BilayerStructure.qml new file mode 100644 index 0000000..04cb0b9 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/BilayerStructure.qml @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements + +import Gui.Globals as Globals + +import "../Components" as Local + +EaElements.GroupColumn { + id: root + property double thirdWidth: (EaStyle.Sizes.sideBarContentWidth - 2 * EaStyle.Sizes.fontPixelSize) / 3 + + Row { + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.Parameter { + width: root.thirdWidth + title: qsTr("Zsep") + units: "nm" + validator: DoubleValidator { bottom: 0 } + text: Globals.BackendWrapper.bilayerStructure.zsep + onEditingFinished: Globals.BackendWrapper.bilayerStructure.zsep = parseFloat(text) + } + EaElements.Parameter { + width: root.thirdWidth + title: qsTr("Nside") + validator: IntValidator { bottom: 1 } + text: Globals.BackendWrapper.bilayerStructure.nside + onEditingFinished: Globals.BackendWrapper.bilayerStructure.nside = parseInt(text) + } + EaElements.Parameter { + width: root.thirdWidth + title: qsTr("Dmin") + units: "nm" + validator: DoubleValidator { bottom: 0.5 } + text: Globals.BackendWrapper.bilayerStructure.dmin + onEditingFinished: Globals.BackendWrapper.bilayerStructure.dmin = parseFloat(text) + } + } + + Column { + width: parent.width + + EaElements.Label { + enabled: false + text: qsTr("Bilayer Fractions") + } + Local.Fractions {} + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Buffer.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Buffer.qml new file mode 100644 index 0000000..7173fcb --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Buffer.qml @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents + +import Gui.Globals as Globals + +EaElements.GroupColumn { + id: root + property double halfWidth: (EaStyle.Sizes.sideBarContentWidth - EaStyle.Sizes.fontPixelSize) / 2 + + // Solvent — single selection (None / TIP3 / Ethanol). + Row { + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.Label { + anchors.verticalCenter: parent.verticalCenter + text: qsTr("Solvent") + } + + EaElements.ComboBox { + width: EaStyle.Sizes.sideBarContentWidth * 0.25 + model: Globals.BackendWrapper.bufferSolventOptions + // Blank collapsed label for the "(None)" sentinel (index 0). + displayText: currentIndex <= 0 ? "" : currentText + Component.onCompleted: currentIndex = model.indexOf(Globals.BackendWrapper.bufferSolvent) + onActivated: (i) => Globals.BackendWrapper.bufferSolvent = model[i] + } + } + + // Buffer components — row list of salts / buffering agents. + Column { + width: parent.width + + EaElements.Label { + enabled: false + text: qsTr("Buffer Components") + } + + EaComponents.ListView { + id: bufferComponentsList + defaultInfoText: qsTr("Load or import buffer components") + multiSelection: true + + columnWidths: [ + EaStyle.Sizes.fontPixelSize * 2.5, + -1, + -1, + EaStyle.Sizes.tableRowHeight + ] + + header: EaComponents.ListViewHeader { + EaComponents.TableViewLabel { + text: qsTr("№") + color: EaStyle.Colors.themeForegroundMinor + horizontalAlignment: Text.AlignHCenter + } + EaComponents.TableViewLabel { + text: qsTr("Name") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Concentration, mM") + color: EaStyle.Colors.themeForegroundMinor + horizontalAlignment: Text.AlignHCenter + } + EaComponents.TableViewLabel {} + } + + model: Globals.BackendWrapper.bufferComponents + + delegateModelAccess: DelegateModel.ReadWrite + + delegate: EaComponents.ListViewDelegate { + required property int index + required property string name + required property real concentration + + EaComponents.TableViewLabel { + text: index + 1 + horizontalAlignment: Text.AlignHCenter + enabled: false + } + EaComponents.TableViewLabel { + text: name + } + EaComponents.ListViewTextInput { + text: concentration + onEditingFinished: concentration = parseFloat(text) + validator: DoubleValidator { bottom: 0; decimals: 3; notation: DoubleValidator.StandardNotation } + } + EaComponents.TableViewButton { + fontIcon: "minus-circle" + ToolTip.text: qsTr("Remove this component") + onClicked: Globals.BackendWrapper.bufferComponentsRemove(index) + } + } + } + } + + Grid { + columns: 2 + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.SideBarButton { + fontIcon: "file-import" + text: qsTr("Load components") + width: root.halfWidth + onClicked: loadBufferComponentLoader.item.open() + } + + EaElements.SideBarButton { + fontIcon: "upload" + text: qsTr("Import components") + width: root.halfWidth + onClicked: importBufferFolderDialog.open() + } + } + + Loader { + id: loadBufferComponentLoader + source: "../Popups/LoadExistingBufferComponent.qml" + } + + // Folder-based import, matching the other Import buttons. + FolderDialog { + id: importBufferFolderDialog + title: qsTr("Import buffer components from a directory") + onAccepted: console.debug(`Import buffer components from folder '${selectedFolder}'`) + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Components.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Components.qml new file mode 100644 index 0000000..cdfa97d --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Components.qml @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents + +import Gui.Globals as Globals + +EaElements.GroupColumn { + id: root + property double halfWidth: (EaStyle.Sizes.sideBarContentWidth - EaStyle.Sizes.fontPixelSize) / 2 + + // Counter-ion options: a "(None)" sentinel (empty c_ion) plus the shared ion library. + readonly property var cIonOptions: { + const lib = Globals.BackendWrapper.ionsAvailable + let opts = [qsTr("(None)")] + for (let i = 0; i < lib.count; ++i) + opts.push(lib.get(i).name) + return opts + } + + EaComponents.ListView { + id: loadedComponents + defaultInfoText: qsTr("Load or create components") + multiSelection: true + + columnWidths: [ + EaStyle.Sizes.fontPixelSize * 2.5, + -1, + EaStyle.Sizes.fontPixelSize * 6, + EaStyle.Sizes.fontPixelSize * 5, + EaStyle.Sizes.fontPixelSize * 4, + EaStyle.Sizes.fontPixelSize * 4, + EaStyle.Sizes.tableRowHeight + ] + + header: EaComponents.ListViewHeader { + EaComponents.TableViewLabel { + text: qsTr("№") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Name") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Type") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("C-ion") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Mint") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Mext") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + color: EaStyle.Colors.themeForegroundMinor + } + } + + model: Globals.BackendWrapper.componentsLoaded + + delegateModelAccess: DelegateModel.ReadWrite + + delegate: EaComponents.ListViewDelegate { + required property int index + required property string name + required property string component_type + required property int mint + required property int mext + required property string c_ion + + EaComponents.TableViewLabel { + text: index + 1 + enabled: false + } + + EaComponents.ListViewTextInput { + text: name + onEditingFinished: name = text + } + + EaComponents.TableViewLabel { + text: component_type + enabled: false + } + + EaComponents.TableViewComboBox { + horizontalAlignment: Text.AlignHCenter + model: root.cIonOptions + // Blank collapsed label for the "(None)" sentinel (0) or no match (-1). + displayText: currentIndex <= 0 ? "" : currentText + Component.onCompleted: currentIndex = c_ion === "" ? 0 : find(c_ion) + onActivated: (i) => c_ion = (i === 0 ? "" : model[i]) + } + + EaComponents.ListViewTextInput { + text: mint + onEditingFinished: mint = parseInt(text) + validator: IntValidator { bottom: 0 } + } + + EaComponents.ListViewTextInput { + text: mext + onEditingFinished: mext = parseInt(text) + validator: IntValidator { bottom: 0 } + } + + EaComponents.TableViewButton { + fontIcon: "minus-circle" + ToolTip.text: qsTr("Remove this component") + onClicked: Globals.BackendWrapper.componentsRemove(index) + } + } + } + + Grid { + columns: 2 + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.SideBarButton { + fontIcon: "file-import" + text: qsTr("Load component(s)") + width: root.halfWidth + onClicked: loadExistingComponentLoader.item.open() + } + + EaElements.SideBarButton { + fontIcon: "upload" + text: qsTr("Import component") + width: root.halfWidth + onClicked: importComponentFolderDialog.open() + } + } + + Loader { + id: loadExistingComponentLoader + source: "../Popups/LoadExistingComponent.qml" + } + + // Folder-based import, matching Model Definition's Import. + FolderDialog { + id: importComponentFolderDialog + title: qsTr("Import a component from a directory") + onAccepted: console.debug(`Import component from folder '${selectedFolder}'`) + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Group1.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Group1.qml deleted file mode 100644 index f2e5c14..0000000 --- a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Group1.qml +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-FileCopyrightText: 2024 EasyApp contributors -// SPDX-License-Identifier: BSD-3-Clause -// © 2024 Contributors to the EasyApp project - -import QtQuick -import QtQuick.Controls - -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents -import EasyApp.Gui.Logic as EaLogic - -import Gui.Globals as Globals - -EaElements.GroupColumn { - -} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Group2.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Group2.qml deleted file mode 100644 index f2e5c14..0000000 --- a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Group2.qml +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-FileCopyrightText: 2024 EasyApp contributors -// SPDX-License-Identifier: BSD-3-Clause -// © 2024 Contributors to the EasyApp project - -import QtQuick -import QtQuick.Controls - -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents -import EasyApp.Gui.Logic as EaLogic - -import Gui.Globals as Globals - -EaElements.GroupColumn { - -} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Group3.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Group3.qml deleted file mode 100644 index f2e5c14..0000000 --- a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Group3.qml +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-FileCopyrightText: 2024 EasyApp contributors -// SPDX-License-Identifier: BSD-3-Clause -// © 2024 Contributors to the EasyApp project - -import QtQuick -import QtQuick.Controls - -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents -import EasyApp.Gui.Logic as EaLogic - -import Gui.Globals as Globals - -EaElements.GroupColumn { - -} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Group4.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Group4.qml deleted file mode 100644 index f2e5c14..0000000 --- a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Group4.qml +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-FileCopyrightText: 2024 EasyApp contributors -// SPDX-License-Identifier: BSD-3-Clause -// © 2024 Contributors to the EasyApp project - -import QtQuick -import QtQuick.Controls - -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents -import EasyApp.Gui.Logic as EaLogic - -import Gui.Globals as Globals - -EaElements.GroupColumn { - -} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Lamellae.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Lamellae.qml new file mode 100644 index 0000000..bb906ab --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Lamellae.qml @@ -0,0 +1,173 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents + +import Gui.Globals as Globals + +import "../Components" as Local + +EaElements.GroupColumn { + id: root + property double thirdWidth: (EaStyle.Sizes.sideBarContentWidth - 2 * EaStyle.Sizes.fontPixelSize) / 3 + + EaComponents.ListView { + id: lamellae + defaultInfoText: qsTr("Add at least one lamella") + multiSelection: false + + columnWidths: [ + EaStyle.Sizes.tableColumnAuto, // № + EaStyle.Sizes.tableColumnAuto, // Rmin, nm + EaStyle.Sizes.tableColumnAuto, // Inner Dmin, nm + EaStyle.Sizes.tableColumnAuto, // Outer Dmin, nm + EaStyle.Sizes.tableColumnAuto, // Shell thickness, nm + EaStyle.Sizes.tableColumnAuto, // Symmetric (checkbox) + EaStyle.Sizes.tableRowHeight // delete button + ] + + header: EaComponents.ListViewHeader { + EaComponents.TableViewLabel { + text: qsTr("№") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Rmin, nm") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Inner dmin, nm") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Outer dmin, nm") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Shell thickness, nm") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Symmetric") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel {} + } + + model: Globals.BackendWrapper.lamellaeItems + + delegateModelAccess: DelegateModel.ReadWrite + + delegate: EaComponents.ListViewDelegate { + required property int index + required property double rmin + required property double innerDmin + required property double outerDmin + required property double shell + required property bool symmetric + + EaComponents.TableViewLabel { + text: index + 1 + enabled: false + } + EaComponents.ListViewTextInput { + text: rmin + onEditingFinished: Globals.BackendWrapper.lamellaeSetRmin(index, parseFloat(text)) + validator: DoubleValidator { bottom: 0.25 } + } + EaComponents.ListViewTextInput { + text: innerDmin + onEditingFinished: Globals.BackendWrapper.lamellaeSetInnerDmin(index, parseFloat(text)) + validator: DoubleValidator { bottom: 0.25 } + } + EaComponents.ListViewTextInput { + text: outerDmin + onEditingFinished: Globals.BackendWrapper.lamellaeSetOuterDmin(index, parseFloat(text)) + validator: DoubleValidator { bottom: 0.25 } + } + EaComponents.ListViewTextInput { + text: shell + onEditingFinished: Globals.BackendWrapper.lamellaeSetShell(index, parseFloat(text)) + validator: DoubleValidator { bottom: 0 } + } + EaComponents.TableViewCheckBox { + checked: symmetric + onToggled: Globals.BackendWrapper.lamellaeSetSymmetric(index, checked) + } + EaComponents.TableViewButton { + fontIcon: "minus-circle" + ToolTip.text: qsTr("Remove this lamella") + onClicked: Globals.BackendWrapper.lamellaeRemove(index) + } + } + } + + Grid { + columns: 1 + + EaElements.SideBarButton { + fontIcon: "plus-circle" + text: qsTr("Add lamella") + width: root.thirdWidth + onClicked: Globals.BackendWrapper.lamellaeAppend({ rmin: 0.5, innerDmin: 0.25, outerDmin: 0.3, shell: 1.0, symmetric: true }) + } + } + + Column { + id: fractionsSection + visible: lamellae.selectedIndexes.length > 0 + width: parent.width + spacing: EaStyle.Sizes.groupBoxSpacing + + readonly property int selectedRow: lamellae.selectedIndexes.length > 0 ? lamellae.selectedIndexes[0].row : -1 + readonly property bool selectedSymmetric: { + // Re-evaluate when the selected row's symmetric flag changes. + void Globals.BackendWrapper.lamellaeItemsRevision + return selectedRow >= 0 && selectedRow < Globals.BackendWrapper.lamellaeItems.count + ? Globals.BackendWrapper.lamellaeItems.get(selectedRow).symmetric + : true + } + readonly property var selectedInnerFractionsModel: { + // Re-evaluate when the per-lamella Fractions arrays are rebuilt. + void Globals.BackendWrapper.lamellaeFractionsRevision + return selectedRow >= 0 ? Globals.BackendWrapper.lamellaeInnerFractionsModelAt(selectedRow) : null + } + readonly property var selectedOuterFractionsModel: { + void Globals.BackendWrapper.lamellaeFractionsRevision + return selectedRow >= 0 ? Globals.BackendWrapper.lamellaeOuterFractionsModelAt(selectedRow) : null + } + + Column { + width: parent.width + + EaElements.Label { + enabled: false + text: fractionsSection.selectedSymmetric + ? qsTr("Lamella %1 Fractions").arg(fractionsSection.selectedRow + 1) + : qsTr("Lamella %1 Inner Leaflet Fractions").arg(fractionsSection.selectedRow + 1) + } + Local.Fractions { + fractionsModel: fractionsSection.selectedInnerFractionsModel + } + } + + Column { + width: parent.width + visible: !fractionsSection.selectedSymmetric + + EaElements.Label { + enabled: false + text: qsTr("Lamella %1 Outer Leaflet Fractions").arg(fractionsSection.selectedRow + 1) + } + Local.Fractions { + fractionsModel: fractionsSection.selectedOuterFractionsModel + } + } + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/LatticeStructure.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/LatticeStructure.qml new file mode 100644 index 0000000..277e9d6 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/LatticeStructure.qml @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements + +import Gui.Globals as Globals + +EaElements.GroupColumn { + id: root + property double thirdWidth: (EaStyle.Sizes.sideBarContentWidth - 2 * EaStyle.Sizes.fontPixelSize) / 3 + property double quarterWidth: (EaStyle.Sizes.sideBarContentWidth - 3 * EaStyle.Sizes.fontPixelSize) / 4 + + Row { + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.Parameter { + width: root.thirdWidth + title: qsTr("Alpha") + units: "⚬" + validator: DoubleValidator { bottom: 0; top: 360 } + text: Globals.BackendWrapper.latticeStructure.alpha + onEditingFinished: Globals.BackendWrapper.latticeStructure.alpha = parseFloat(text) + } + EaElements.Parameter { + width: root.thirdWidth + title: qsTr("Theta") + units: "⚬" + validator: DoubleValidator { bottom: 0; top: 180 } + text: Globals.BackendWrapper.latticeStructure.theta + onEditingFinished: Globals.BackendWrapper.latticeStructure.theta = parseFloat(text) + } + EaElements.Parameter { + width: root.thirdWidth + title: qsTr("Sbuff") + units: "nm" + validator: DoubleValidator { bottom: 0 } + text: Globals.BackendWrapper.latticeStructure.sbuff + onEditingFinished: Globals.BackendWrapper.latticeStructure.sbuff = parseFloat(text) + } + } + + Row { + spacing: EaStyle.Sizes.fontPixelSize + + Column { + width: root.quarterWidth + + EaElements.Label { + enabled: false + text: qsTr("Type") + } + EaElements.ComboBox { + width: parent.width + model: Globals.BackendWrapper.latticeStructure.latticeTypes + Component.onCompleted: currentIndex = model.indexOf(Globals.BackendWrapper.latticeStructure.latticeType) + onActivated: (i) => Globals.BackendWrapper.latticeStructure.latticeType = model[i] + } + } + EaElements.Parameter { + width: root.quarterWidth + title: qsTr("Nlatx") + validator: IntValidator { bottom: 1 } + text: Globals.BackendWrapper.latticeStructure.nlatx + onEditingFinished: Globals.BackendWrapper.latticeStructure.nlatx = parseInt(text) + } + EaElements.Parameter { + width: root.quarterWidth + title: qsTr("Nlaty") + validator: IntValidator { bottom: 1 } + text: Globals.BackendWrapper.latticeStructure.nlaty + onEditingFinished: Globals.BackendWrapper.latticeStructure.nlaty = parseInt(text) + } + EaElements.Parameter { + width: root.quarterWidth + title: qsTr("Nlatz") + validator: IntValidator { bottom: 1 } + text: Globals.BackendWrapper.latticeStructure.nlatz + onEditingFinished: Globals.BackendWrapper.latticeStructure.nlatz = parseInt(text) + } + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Layers.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Layers.qml new file mode 100644 index 0000000..a05607c --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/Layers.qml @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents + +import Gui.Globals as Globals + +import "../Components" as Local + +EaElements.GroupColumn { + id: root + property double thirdWidth: (EaStyle.Sizes.sideBarContentWidth - 2 * EaStyle.Sizes.fontPixelSize) / 3 + + EaComponents.ListView { + id: layers + defaultInfoText: qsTr("Add at least one layer") + multiSelection: false + + columnWidths: [ + EaStyle.Sizes.tableColumnAuto, // № + EaStyle.Sizes.tableColumnFlex, // filler + EaStyle.Sizes.tableColumnAuto, // Dmin, nm + EaStyle.Sizes.tableColumnAuto, // Rmin, nm + EaStyle.Sizes.tableRowHeight // delete button + ] + + header: EaComponents.ListViewHeader { + EaComponents.TableViewLabel { + text: qsTr("№") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel {} + EaComponents.TableViewLabel { + text: qsTr("Dmin, nm") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Rmin, nm") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel {} + } + + model: Globals.BackendWrapper.layersItems + + delegateModelAccess: DelegateModel.ReadWrite + + delegate: EaComponents.ListViewDelegate { + required property int index + required property double dmin + required property double rmin + + EaComponents.TableViewLabel { + text: index + 1 + enabled: false + } + EaComponents.TableViewLabel {} + EaComponents.ListViewTextInput { + text: dmin + onEditingFinished: Globals.BackendWrapper.layersSetDmin(index, parseFloat(text)) + validator: DoubleValidator { bottom: 0.25 } + } + EaComponents.ListViewTextInput { + text: rmin + onEditingFinished: Globals.BackendWrapper.layersSetRmin(index, parseFloat(text)) + validator: DoubleValidator { bottom: 0.25 } + } + EaComponents.TableViewButton { + fontIcon: "minus-circle" + ToolTip.text: qsTr("Remove this layer") + onClicked: Globals.BackendWrapper.layersRemove(index) + } + } + } + + Grid { + columns: 1 + + EaElements.SideBarButton { + fontIcon: "plus-circle" + text: qsTr("Add layer") + width: root.thirdWidth + onClicked: Globals.BackendWrapper.layersAppend({ dmin: 0.5, rmin: 0.25 }) + } + } + + Column { + id: fractionsSection + visible: layers.selectedIndexes.length > 0 + width: parent.width + + readonly property int selectedRow: layers.selectedIndexes.length > 0 ? layers.selectedIndexes[0].row : -1 + readonly property var selectedFractionsModel: { + // Re-evaluate when the per-layer Fractions array is rebuilt. + void Globals.BackendWrapper.layersFractionsRevision + return selectedRow >= 0 ? Globals.BackendWrapper.layersFractionsModelAt(selectedRow) : null + } + + EaElements.Label { + enabled: false + text: qsTr("Layer %1 Fractions").arg(fractionsSection.selectedRow >= 0 ? fractionsSection.selectedRow + 1 : 1) + } + Local.Fractions { + fractionsModel: fractionsSection.selectedFractionsModel + } + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/MonolayerStructure.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/MonolayerStructure.qml new file mode 100644 index 0000000..c8b84b5 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/MonolayerStructure.qml @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements + +import Gui.Globals as Globals + +import "../Components" as Local + +EaElements.GroupColumn { + id: root + property double thirdWidth: (EaStyle.Sizes.sideBarContentWidth - 2 * EaStyle.Sizes.fontPixelSize) / 3 + + Row { + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.Parameter { + width: root.thirdWidth + title: qsTr("Zsep") + units: "nm" + validator: DoubleValidator { bottom: 0 } + text: Globals.BackendWrapper.monolayerStructure.zsep + onEditingFinished: Globals.BackendWrapper.monolayerStructure.zsep = parseFloat(text) + } + EaElements.Parameter { + width: root.thirdWidth + title: qsTr("Nside") + validator: IntValidator { bottom: 1 } + text: Globals.BackendWrapper.monolayerStructure.nside + onEditingFinished: Globals.BackendWrapper.monolayerStructure.nside = parseInt(text) + } + EaElements.Parameter { + width: root.thirdWidth + title: qsTr("Dmin") + units: "nm" + validator: DoubleValidator { bottom: 0.5 } + text: Globals.BackendWrapper.monolayerStructure.dmin + onEditingFinished: Globals.BackendWrapper.monolayerStructure.dmin = parseFloat(text) + } + } + + Column { + width: parent.width + + EaElements.Label { + enabled: false + text: qsTr("Monolayer Fractions") + } + Local.Fractions {} + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/RingStructure.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/RingStructure.qml new file mode 100644 index 0000000..f155038 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/RingStructure.qml @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements + +import Gui.Globals as Globals + +import "../Components" as Local + +EaElements.GroupColumn { + id: root + property double quarterWidth: (EaStyle.Sizes.sideBarContentWidth - 3 * EaStyle.Sizes.fontPixelSize) / 4 + + Row { + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.Parameter { + width: root.quarterWidth + title: qsTr("Dmin") + units: "nm" + validator: DoubleValidator { bottom: 0.5 } + text: Globals.BackendWrapper.ringStructure.dmin + onEditingFinished: Globals.BackendWrapper.ringStructure.dmin = parseFloat(text) + } + EaElements.Parameter { + width: root.quarterWidth + title: qsTr("Rmin") + units: "nm" + validator: DoubleValidator { bottom: 0.25 } + text: Globals.BackendWrapper.ringStructure.rmin + onEditingFinished: Globals.BackendWrapper.ringStructure.rmin = parseFloat(text) + } + EaElements.Parameter { + width: root.quarterWidth + title: qsTr("Alpha") + units: "⚬" + validator: DoubleValidator { bottom: 0; top: 360 } + text: Globals.BackendWrapper.ringStructure.alpha + onEditingFinished: Globals.BackendWrapper.ringStructure.alpha = parseFloat(text) + } + EaElements.Parameter { + width: root.quarterWidth + title: qsTr("Theta") + units: "⚬" + validator: DoubleValidator { bottom: 0; top: 180 } + text: Globals.BackendWrapper.ringStructure.theta + onEditingFinished: Globals.BackendWrapper.ringStructure.theta = parseFloat(text) + } + } + + Row { + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.CheckBox { + width: EaStyle.Sizes.sideBarContentWidth * 0.15 + height: EaStyle.Sizes.fontPixelSize * 3 + text: qsTr("Fxz") + checked: Globals.BackendWrapper.ringStructure.fxz + onToggled: Globals.BackendWrapper.ringStructure.fxz = checked + } + EaElements.CheckBox { + width: EaStyle.Sizes.sideBarContentWidth * 0.15 + height: EaStyle.Sizes.fontPixelSize * 3 + text: qsTr("Rev") + checked: Globals.BackendWrapper.ringStructure.rev + onToggled: Globals.BackendWrapper.ringStructure.rev = checked + } + } + + Column { + width: parent.width + + EaElements.Label { + enabled: false + text: qsTr("Ring Fractions") + } + Local.Fractions {} + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/RodStructure.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/RodStructure.qml new file mode 100644 index 0000000..62a84ed --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/RodStructure.qml @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements + +import Gui.Globals as Globals + +import "../Components" as Local + +EaElements.GroupColumn { + id: root + property double thirdWidth: (EaStyle.Sizes.sideBarContentWidth - 2 * EaStyle.Sizes.fontPixelSize) / 3 + + Row { + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.Parameter { + width: root.thirdWidth + title: qsTr("Dmin") + units: "nm" + validator: DoubleValidator { bottom: 0.5 } + text: Globals.BackendWrapper.rodStructure.dmin + onEditingFinished: Globals.BackendWrapper.rodStructure.dmin = parseFloat(text) + } + EaElements.Parameter { + width: root.thirdWidth + title: qsTr("Rmin") + units: "nm" + validator: DoubleValidator { bottom: 0.25 } + text: Globals.BackendWrapper.rodStructure.rmin + onEditingFinished: Globals.BackendWrapper.rodStructure.rmin = parseFloat(text) + } + EaElements.Parameter { + width: root.thirdWidth + title: qsTr("Turns") + validator: IntValidator { bottom: 1 } + text: Globals.BackendWrapper.rodStructure.turns + onEditingFinished: Globals.BackendWrapper.rodStructure.turns = parseInt(text) + } + } + + Row { + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.CheckBox { + width: EaStyle.Sizes.sideBarContentWidth * 0.15 + height: EaStyle.Sizes.fontPixelSize * 3 + text: qsTr("Fxz") + checked: Globals.BackendWrapper.rodStructure.fxz + onToggled: Globals.BackendWrapper.rodStructure.fxz = checked + } + EaElements.CheckBox { + width: EaStyle.Sizes.sideBarContentWidth * 0.15 + height: EaStyle.Sizes.fontPixelSize * 3 + text: qsTr("Rev") + checked: Globals.BackendWrapper.rodStructure.rev + onToggled: Globals.BackendWrapper.rodStructure.rev = checked + } + } + + Column { + width: parent.width + + EaElements.Label { + enabled: false + text: qsTr("Rod Fractions") + } + Local.Fractions {} + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/SampleModel.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/SampleModel.qml new file mode 100644 index 0000000..fce746f --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/SampleModel.qml @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents + +import Gui.Globals as Globals + +EaElements.GroupColumn { + id: root + property double halfWidth: (EaStyle.Sizes.sideBarContentWidth - EaStyle.Sizes.fontPixelSize) / 2 + + EaComponents.ListView { + id: loadedSampleModel + defaultInfoText: qsTr("Get started by loading or importing a model") + multiSelection: false + + columnWidths: [ + EaStyle.Sizes.fontPixelSize * 10, + EaStyle.Sizes.fontPixelSize * 6, + EaStyle.Sizes.fontPixelSize * 7, + -1 + ] + + header: EaComponents.ListViewHeader { + EaComponents.TableViewLabel { + text: qsTr("Name") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Type") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Shape") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Description") + color: EaStyle.Colors.themeForegroundMinor + } + } + + model: Globals.BackendWrapper.sampleModelLoaded + + delegate: EaComponents.ListViewDelegate { + required property var modelData + required property int index + + EaComponents.ListViewTextInput { + text: modelData ? modelData.name : "" + onEditingFinished: Globals.BackendWrapper.sampleModelUpdateField("name", text) + } + EaComponents.TableViewComboBox { + horizontalAlignment: Text.AlignHCenter + model: Globals.BackendWrapper.sampleModelTypes + Component.onCompleted: currentIndex = model.indexOf(modelData.type) + onActivated: (i) => Globals.BackendWrapper.sampleModelUpdateField("type", model[i]) + } + EaComponents.TableViewComboBox { + horizontalAlignment: Text.AlignHCenter + model: Globals.BackendWrapper.sampleModelStructureTypes + Component.onCompleted: currentIndex = model.indexOf(modelData.structure_type) + onActivated: (i) => Globals.BackendWrapper.sampleModelUpdateField("structure_type", model[i]) + } + EaComponents.ListViewTextInput { + text: modelData ? modelData.description : "" + onEditingFinished: Globals.BackendWrapper.sampleModelUpdateField("description", text) + } + } + } + + Grid { + columns: 2 + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.SideBarButton { + fontIcon: "file-import" + text: qsTr("Load model") + width: root.halfWidth + onClicked: loadExistingModelLoader.item.open() + } + + EaElements.SideBarButton { + fontIcon: "upload" + text: qsTr("Import model") + width: root.halfWidth + onClicked: importModelFolderDialog.open() + } + } + + Loader { + id: loadExistingModelLoader + source: "../Popups/LoadExistingModel.qml" + } + + // Folder-based import: Qt's file dialogs can't accept a folder and a file at once. + FolderDialog { + id: importModelFolderDialog + title: qsTr("Import a model from a directory") + onAccepted: console.debug(`Import model from folder '${selectedFolder}'`) + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/VesicleStructure.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/VesicleStructure.qml new file mode 100644 index 0000000..d63d240 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Groups/VesicleStructure.qml @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls + +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements + +import Gui.Globals as Globals + +EaElements.GroupColumn { + + Column { + width: EaStyle.Sizes.sideBarContentWidth * 0.2 + + EaElements.Label { + enabled: false + text: qsTr("Fill") + } + EaElements.ComboBox { + width: parent.width + model: ["FIBO", "RINGS", "RINGS0"] + Component.onCompleted: currentIndex = model.indexOf(Globals.BackendWrapper.vesicleStructure.fill) + onActivated: (i) => Globals.BackendWrapper.vesicleStructure.fill = model[i] + } + } + + Row { + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.CheckBox { + width: EaStyle.Sizes.sideBarContentWidth * 0.15 + height: EaStyle.Sizes.fontPixelSize * 3 + text: qsTr("Fxz") + checked: Globals.BackendWrapper.vesicleStructure.fxz + onToggled: Globals.BackendWrapper.vesicleStructure.fxz = checked + } + EaElements.CheckBox { + width: EaStyle.Sizes.sideBarContentWidth * 0.15 + height: EaStyle.Sizes.fontPixelSize * 3 + text: qsTr("Rev") + checked: Globals.BackendWrapper.vesicleStructure.rev + onToggled: Globals.BackendWrapper.vesicleStructure.rev = checked + } + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Layout.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Layout.qml index d13c364..9431243 100644 --- a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Layout.qml +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Layout.qml @@ -5,40 +5,121 @@ import QtQuick import QtQuick.Controls -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Components as EaComponents import Gui.Globals as Globals - EaComponents.SideBarColumn { EaElements.GroupBox { - title: qsTr('Group 1') - icon: 'rocket' + title: qsTr("Model Definition") + icon: "tag" + collapsed: false + + Loader { source: "Groups/SampleModel.qml" } + } + + EaElements.GroupBox { + title: qsTr("Components") + icon: "puzzle-piece" + + Loader { source: "Groups/Components.qml" } + } + + EaElements.GroupBox { + title: qsTr("Ring Structure Definition") + icon: "project-diagram" + visible: Globals.BackendWrapper.sampleModelCurrentStructureType === "Ring" - Loader { source: 'Groups/Group1.qml' } + Loader { source: "Groups/RingStructure.qml" } } EaElements.GroupBox { - title: qsTr('Group 2') - icon: 'rocket' + title: qsTr("Ball Structure Definition") + icon: "project-diagram" + visible: Globals.BackendWrapper.sampleModelCurrentStructureType === "Ball" - Loader { source: 'Groups/Group2.qml' } + Loader { source: "Groups/BallStructure.qml" } } EaElements.GroupBox { - title: qsTr('Group 3') - icon: 'rocket' + title: qsTr("Vesicle Structure Definition") + icon: "project-diagram" + visible: Globals.BackendWrapper.sampleModelCurrentStructureType === "Vesicle" - Loader { source: 'Groups/Group3.qml' } + Loader { source: "Groups/VesicleStructure.qml" } } EaElements.GroupBox { - title: qsTr('Group 4') - icon: 'rocket' + title: qsTr("Rod Structure Definition") + icon: "project-diagram" + visible: Globals.BackendWrapper.sampleModelCurrentStructureType === "Rod" - Loader { source: 'Groups/Group4.qml' } + Loader { source: "Groups/RodStructure.qml" } } + EaElements.GroupBox { + title: qsTr("Bilayer Structure Definition") + icon: "project-diagram" + visible: Globals.BackendWrapper.sampleModelCurrentStructureType === "Bilayer" + + Loader { source: "Groups/BilayerStructure.qml" } + } + + EaElements.GroupBox { + title: qsTr("Monolayer Structure Definition") + icon: "project-diagram" + visible: Globals.BackendWrapper.sampleModelCurrentStructureType === "Monolayer" + + Loader { source: "Groups/MonolayerStructure.qml" } + } + + EaElements.GroupBox { + title: qsTr("Layers") + icon: "layer-group" + visible: Globals.BackendWrapper.sampleModelCurrentStructureType === "Ball" + + Loader { source: "Groups/Layers.qml" } + } + + EaElements.GroupBox { + title: qsTr("Lamellae") + icon: "layer-group" + visible: Globals.BackendWrapper.sampleModelCurrentStructureType === "Vesicle" + + Loader { source: "Groups/Lamellae.qml" } + } + + EaElements.GroupBox { + title: qsTr("Lattice Parameters") + icon: "vector-square" + visible: Globals.BackendWrapper.sampleModelCurrentType === "Lattice" + + Loader { source: "Groups/LatticeStructure.qml" } + } + + EaElements.GroupBox { + title: qsTr("Buffer") + icon: "flask" + + Loader { source: "Groups/Buffer.qml" } + } + + // Centered "Assemble" call-to-action below the groups. + Item { + width: parent.width + height: assembleButton.height + EaStyle.Sizes.fontPixelSize + + EaElements.SideBarButton { + id: assembleButton + anchors.centerIn: parent + text: qsTr("Assemble") + fontIcon: "cubes" + width: EaStyle.Sizes.sideBarContentWidth + enabled: Globals.BackendWrapper.sampleModelLoaded.length > 0 + onClicked: console.debug("Assemble clicked") + } + } } diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/CreateNewComponent.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/CreateNewComponent.qml new file mode 100644 index 0000000..cfc4383 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/CreateNewComponent.qml @@ -0,0 +1,143 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +import EasyApplication.Gui.Globals as EaGlobals +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Components as EaComponents +import EasyApplication.Gui.Elements as EaElements + +import Gui.Globals as Globals + + +EaElements.Dialog{ + id: componentCreationDialog + + property int inputFieldWidth: EaStyle.Sizes.fontPixelSize * 35 + + title: qsTr("Create a new Component") + standardButtons: Dialog.Ok | Dialog.Cancel + + onAccepted: { + Globals.BackendWrapper.componentsAppend({ + name: componentNameField.text, + component_type: sampleModelTypeField.currentText, + c_ion: "", + mint: 0, + mext: 0 + }) + } + + Column { + spacing: EaStyle.Sizes.fontPixelSize + + Row { + property int halfFieldWidth: (componentCreationDialog.inputFieldWidth - spacing) / 2 + spacing: EaStyle.Sizes.fontPixelSize + + Column { + EaElements.Label { + enabled: false + text: qsTr("Name") + } + EaElements.TextField { + id: componentNameField + implicitWidth: parent.parent.halfFieldWidth + horizontalAlignment: TextInput.AlignLeft + validator: RegularExpressionValidator { regularExpression: /^[a-zA-Z][a-zA-Z0-9_\-\.]{1,30}$/ } + placeholderText: qsTr("(optional) Enter Component name here") + } + } + + Column { + EaElements.Label { + enabled: false + text: qsTr("Component type") + } + EaElements.ComboBox { + id: sampleModelTypeField + implicitWidth: parent.parent.halfFieldWidth + model: [qsTr("Other"), qsTr("Lipid"), qsTr("Surfactant")] + } + } + } + + Column { + EaElements.Label { + enabled: false + text: qsTr("SMILES string") + } + + EaElements.TextField { + implicitWidth: componentCreationDialog.inputFieldWidth + horizontalAlignment: TextInput.AlignLeft + placeholderText: qsTr("(optional) Define component using SMILES") + } + } + + Column { + EaElements.Label { + enabled: false + text: qsTr("Paths") + } + + EaComponents.ListView { + id: filePaths + defaultInfoText: qsTr("No files added") + enabled: true + maxRowCountShow: 2 + width: componentCreationDialog.inputFieldWidth + scrollBarInteractive: false + + columnWidths: [ + -1, + EaStyle.Sizes.tableRowHeight + ] + + model: Globals.BackendWrapper.componentsPendingFilePaths + + delegate: EaComponents.ListViewDelegate { + required property int index + required property url path + + EaComponents.TableViewLabel { + id: pathColumn + text: path + elide: Text.ElideLeft + horizontalAlignment: Text.Alignleft + + leftPadding: EaStyle.Sizes.fontPixelSize * 0.5 + } + + EaComponents.TableViewButton { + id: deleteRowColumn + fontIcon: "minus-circle" + ToolTip.text: qsTr("Remove this file") + onClicked: Globals.BackendWrapper.componentsRemovePendingFilePath(index) + } + } + } + } + Column { + EaElements.SideBarButton { + fontIcon: "upload" + text: qsTr("Add files") + width: componentCreationDialog.width * 0.3164 + + onClicked: { + console.debug(`Clicking '${text}' button ::: ${this}`) + Globals.References.pages.samplemodel.sidebar.basic.popups.openAssetFile.open() + } + + Loader { + source: "../Popups/OpenAssetFile.qml" + } + } + } + } +} + diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/LoadExistingBufferComponent.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/LoadExistingBufferComponent.qml new file mode 100644 index 0000000..0ed9afd --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/LoadExistingBufferComponent.qml @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +import EasyApplication.Gui.Globals as EaGlobals +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Components as EaComponents +import EasyApplication.Gui.Elements as EaElements + +import Gui.Globals as Globals + + +EaElements.Dialog { + id: bufferComponentLoadDialog + + property int inputFieldWidth: EaStyle.Sizes.fontPixelSize * 35 + + title: qsTr("Load Buffer Components from the Asset Library") + standardButtons: Dialog.Ok | Dialog.Cancel + + onAccepted: { + var indexes = loadBufferComponentListView.selectedIndexes + + for (var i = 0; i < indexes.length; ++i) { + var item = Globals.BackendWrapper.bufferComponentsAvailable.get(indexes[i].row) + Globals.BackendWrapper.bufferComponentsAppend(item) + } + loadBufferComponentListView.clearSelection() + } + onRejected: { + loadBufferComponentListView.clearSelection() + } + + Column { + EaElements.Label { + enabled: false + text: qsTr("Available in the Asset Library") + } + EaComponents.ListView { + id: loadBufferComponentListView + defaultInfoText: qsTr("No buffer components found") + multiSelection: true + columnWidths: [ + EaStyle.Sizes.fontPixelSize * 2.5, + EaStyle.Sizes.fontPixelSize * 10, + -1, + ] + + header: EaComponents.ListViewHeader { + EaComponents.TableViewLabel { + text: qsTr("№") + color: EaStyle.Colors.themeForegroundMinor + horizontalAlignment: Text.AlignHCenter + } + EaComponents.TableViewLabel { + text: qsTr("Name") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Description") + color: EaStyle.Colors.themeForegroundMinor + } + } + + model: Globals.BackendWrapper.bufferComponentsAvailable + + delegate: EaComponents.ListViewDelegate { + required property int index + required property string name + required property string description + + EaComponents.TableViewLabel { + text: index + 1 + horizontalAlignment: Text.AlignHCenter + enabled: false + } + EaComponents.TableViewLabel { + text: name + } + EaComponents.TableViewLabel { + text: description + ToolTip.text: description + } + } + } + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/LoadExistingComponent.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/LoadExistingComponent.qml new file mode 100644 index 0000000..a01c201 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/LoadExistingComponent.qml @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +import EasyApplication.Gui.Globals as EaGlobals +import EasyApplication.Gui.Components as EaComponents +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Style as EaStyle + +import Gui.Globals as Globals + + +EaElements.Dialog{ + id: sampleModelLoadDialog + + property int inputFieldWidth: EaStyle.Sizes.fontPixelSize * 35 + property alias availableComponentsModel: availableComponentsModel + + title: qsTr("Load Components from the Asset Library") + standardButtons: Dialog.Ok | Dialog.Cancel + + onAccepted: { + let selected = loadComponentListView.selectedIndexes + for (let i = 0; i < selected.length; ++i) { + var row = selected[i].row + var item = availableComponentsModel.get(row) + + Globals.BackendWrapper.componentsAppend({ + name: item.name, + component_type: item.component_type, + mint: item.mint, + mext: item.mext, + c_ion: item.c_ion + }) + } + loadComponentListView.clearSelection() + } + onRejected: { + loadComponentListView.clearSelection() + } + + Column { + EaElements.Label { + enabled: false + text: qsTr("Available in the Asset Library") + } + + EaComponents.ListView { + id: loadComponentListView + defaultInfoText: qsTr("No models found") + multiSelection: true + + columnWidths: [ + EaStyle.Sizes.fontPixelSize * 2.5, + -1, + EaStyle.Sizes.fontPixelSize * 8, + EaStyle.Sizes.fontPixelSize * 4.5, + EaStyle.Sizes.fontPixelSize * 6, + EaStyle.Sizes.fontPixelSize * 6, + EaStyle.Sizes.tableRowHeight, + ] + + header: EaComponents.ListViewHeader { + EaComponents.TableViewLabel { + text: qsTr("№") + color: EaStyle.Colors.themeForegroundMinor + horizontalAlignment: Text.AlignHCenter + } + EaComponents.TableViewLabel { + text: qsTr("Name") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Type") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("C-ion") + color: EaStyle.Colors.themeForegroundMinor + horizontalAlignment: Text.AlignHCenter + } + EaComponents.TableViewLabel { + text: qsTr("Mint") + color: EaStyle.Colors.themeForegroundMinor + horizontalAlignment: Text.AlignHCenter + } + EaComponents.TableViewLabel { + text: qsTr("Mext") + color: EaStyle.Colors.themeForegroundMinor + horizontalAlignment: Text.AlignHCenter + } + } + + model: ListModel { + id: availableComponentsModel + ListElement { name: "DPPC"; component_type: "Lipid"; c_ion: ""; mint: 0; mext: 130 } + ListElement { name: "DOPC"; component_type: "Lipid"; c_ion: ""; mint: 0; mext: 138 } + ListElement { name: "POPC"; component_type: "Lipid"; c_ion: ""; mint: 0; mext: 134 } + ListElement { name: "DMPC"; component_type: "Lipid"; c_ion: ""; mint: 0; mext: 118 } + ListElement { name: "Cholesterol"; component_type: "Lipid"; c_ion: ""; mint: 0; mext: 74 } + ListElement { name: "SDS"; component_type: "Surfactant"; c_ion: "Na+"; mint: 0; mext: 42 } + ListElement { name: "CTAB"; component_type: "Surfactant"; c_ion: "Br-"; mint: 0; mext: 62 } + ListElement { name: "Triton-X100"; component_type: "Surfactant"; c_ion: ""; mint: 0; mext: 85 } + ListElement { name: "Tween-20"; component_type: "Surfactant"; c_ion: ""; mint: 0; mext: 98 } + ListElement { name: "D2O-buffer"; component_type: "Other"; c_ion: ""; mint: 0; mext: 3 } + } + + delegateModelAccess: DelegateModel.ReadOnly + + delegate: EaComponents.ListViewDelegate { + required property int index + required property string name + required property string component_type + required property string c_ion + required property int mint + required property int mext + + EaComponents.TableViewLabel { + text: index + 1 + horizontalAlignment: Text.AlignHCenter + enabled: false + } + EaComponents.TableViewLabel { + text: name + } + EaComponents.TableViewLabel { + text: component_type + } + EaComponents.TableViewLabel { + text: c_ion + horizontalAlignment: Text.AlignHCenter + } + EaComponents.TableViewLabel { + text: mint + horizontalAlignment: Text.AlignHCenter + } + EaComponents.TableViewLabel { + text: mext + horizontalAlignment: Text.AlignHCenter + } + EaComponents.TableViewButton { + fontIcon: "minus-circle" + ToolTip.text: qsTr("Remove this component") + onClicked: availableComponentsModel.remove(index) + } + } + } + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/LoadExistingIon.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/LoadExistingIon.qml new file mode 100644 index 0000000..de267fb --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/LoadExistingIon.qml @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +import EasyApplication.Gui.Globals as EaGlobals +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Components as EaComponents +import EasyApplication.Gui.Elements as EaElements + +import Gui.Globals as Globals + + +EaElements.Dialog { + id: ionLoadDialog + + property int inputFieldWidth: EaStyle.Sizes.fontPixelSize * 25 + + title: qsTr("Load an Ion from the Asset Library") + standardButtons: Dialog.Ok | Dialog.Cancel + + // Block confirmation when the slot is full or no row is selected. + Component.onCompleted: { + var ok = standardButton(Dialog.Ok) + if (ok) { + ok.enabled = Qt.binding(function () { + if (!Globals.BackendWrapper.ionsLoaded + || Globals.BackendWrapper.ionsLoaded.count >= 2) return false + return loadIonListView.selectedIndexes.length > 0 + }) + } + } + + onAccepted: { + var indexes = loadIonListView.selectedIndexes + + if (indexes.length > 0) { + var row = indexes[0].row + var item = Globals.BackendWrapper.ionsAvailable.get(row) + Globals.BackendWrapper.ionsAppend({ name: item.name }) + loadIonListView.clearSelection() + } + } + onRejected: { + loadIonListView.clearSelection() + } + + Column { + EaElements.Label { + enabled: false + text: qsTr("Available in the Asset Library") + } + EaComponents.ListView { + id: loadIonListView + defaultInfoText: qsTr("No ions found") + multiSelection: false + columnWidths: [ + EaStyle.Sizes.fontPixelSize * 2.5, + -1, + ] + + header: EaComponents.ListViewHeader { + EaComponents.TableViewLabel { + text: qsTr("№") + color: EaStyle.Colors.themeForegroundMinor + horizontalAlignment: Text.AlignHCenter + } + EaComponents.TableViewLabel { + text: qsTr("Name") + color: EaStyle.Colors.themeForegroundMinor + } + } + + model: Globals.BackendWrapper.ionsAvailable + + delegate: EaComponents.ListViewDelegate { + required property int index + required property string name + + EaComponents.TableViewLabel { + text: index + 1 + horizontalAlignment: Text.AlignHCenter + enabled: false + } + EaComponents.TableViewLabel { + text: name + } + } + } + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/LoadExistingModel.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/LoadExistingModel.qml new file mode 100644 index 0000000..2b049b8 --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/LoadExistingModel.qml @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +import EasyApplication.Gui.Globals as EaGlobals +import EasyApplication.Gui.Components as EaComponents +import EasyApplication.Gui.Elements as EaElements +import EasyApplication.Gui.Style as EaStyle + +import Gui.Globals as Globals + + + +EaElements.Dialog{ + id: sampleModelLoadDialog + + property int inputFieldWidth: EaStyle.Sizes.fontPixelSize * 35 + + title: qsTr("Load a Sample Model from the Asset Library") + standardButtons: Dialog.Ok | Dialog.Cancel + + onAccepted: { + var indexes = loadModelListView.selectedIndexes + + if (indexes.length > 0) { + var row = indexes[0].row + var item = Globals.BackendWrapper.sampleModelAvailable.get(row) + Globals.BackendWrapper.sampleModelSetLoaded(item) + loadModelListView.clearSelection() + } + } + onRejected: { + loadModelListView.clearSelection() + } + + Column { + EaElements.Label { + enabled: false + text: qsTr("Available in the Asset Library") + } + EaComponents.ListView { + id: loadModelListView + defaultInfoText: qsTr("No models found") + multiSelection: false + columnWidths: [ + EaStyle.Sizes.fontPixelSize * 2.5, + EaStyle.Sizes.fontPixelSize * 10, + EaStyle.Sizes.fontPixelSize * 8, + EaStyle.Sizes.fontPixelSize * 6, + -1, + EaStyle.Sizes.tableRowHeight, + ] + + header: EaComponents.ListViewHeader { + EaComponents.TableViewLabel { + text: qsTr("№") + color: EaStyle.Colors.themeForegroundMinor + horizontalAlignment: Text.AlignHCenter + } + EaComponents.TableViewLabel { + text: qsTr("Name") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Shape") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Type") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel { + text: qsTr("Description") + color: EaStyle.Colors.themeForegroundMinor + } + EaComponents.TableViewLabel {} // filler + } + + model: Globals.BackendWrapper.sampleModelAvailable + + delegate: EaComponents.ListViewDelegate { + required property int index + required property string name + required property string structure_type + required property string type + required property string description + + EaComponents.TableViewLabel { + text: index + 1 + horizontalAlignment: Text.AlignHCenter + enabled: false + } + EaComponents.TableViewLabel { + text: name + } + EaComponents.TableViewLabel { + text: structure_type + } + EaComponents.TableViewLabel { + text: type + } + EaComponents.TableViewLabel { + text: description + } + EaComponents.TableViewButton { + fontIcon: "minus-circle" + ToolTip.text: qsTr("Remove this component") + onClicked: Globals.BackendWrapper.sampleModelRemoveFromCatalog(index) + } + } + } + } +} diff --git a/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/OpenAssetFile.qml b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/OpenAssetFile.qml new file mode 100644 index 0000000..f0180ea --- /dev/null +++ b/src/easyshapes_app/Gui/Pages/SampleModel/Sidebar/Basic/Popups/OpenAssetFile.qml @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +import EasyApplication.Gui.Globals as EaGlobals +import EasyApplication.Gui.Components as EaComponents + +import Gui.Globals as Globals + + +FileDialog{ + fileMode: FileDialog.OpenFiles + nameFilters: ["Any (*)", "Structure files (*.gro *.pdb .*xyz)", "Topology files (*.itp)", "Smiles files (*.sml)"] + + onAccepted: { + for (let i = 0; i < selectedFiles.length; ++i) + Globals.BackendWrapper.componentsAppendPendingFilePath(selectedFiles[i]) + } + + Component.onCompleted: { + Globals.References.pages.samplemodel.sidebar.basic.popups.openAssetFile = this + } + +} diff --git a/src/easyshapes_app/Gui/Pages/Toolbox/Layout.qml b/src/easyshapes_app/Gui/Pages/Toolbox/Layout.qml deleted file mode 100644 index 360aa6f..0000000 --- a/src/easyshapes_app/Gui/Pages/Toolbox/Layout.qml +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2024 EasyApp contributors -// SPDX-License-Identifier: BSD-3-Clause -// © 2024 Contributors to the EasyApp project - -import QtQuick -import QtQuick.Controls - -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents - -import Gui.Globals as Globals - - -EaComponents.ContentPage { - - mainView: EaComponents.MainContent { - tabs: [ - EaElements.TabButton { text: qsTr('Image') }, - EaElements.TabButton { text: qsTr('EaElements.GraphsView') }, - EaElements.TabButton { text: qsTr('EaElements.TextArea (Plain)') }, - EaElements.TabButton { text: qsTr('EaElements.TextArea (Rich)') } - ] - - items: [ - Loader { source: 'MainArea/Image.qml' }, - Loader { source: 'MainArea/GraphsView.qml' }, - Loader { source: 'MainArea/TextAreaPlain.qml' }, - Loader { source: 'MainArea/TextAreaRich.qml' } - ] - } - - sideBar: EaComponents.SideBar { - tabs: [ - EaElements.TabButton { text: qsTr('Basic controls') } - ] - - items: [ - Loader { source: 'Sidebar/Basic/Layout.qml' } - ] - - continueButton.visible: false - - } - - Component.onCompleted: console.debug(`Toolbox page loaded ::: ${this}`) - Component.onDestruction: console.debug(`Toolbox page destroyed ::: ${this}`) - -} diff --git a/src/easyshapes_app/Gui/Pages/Toolbox/MainArea/GraphsView.qml b/src/easyshapes_app/Gui/Pages/Toolbox/MainArea/GraphsView.qml deleted file mode 100644 index 9f2fd2d..0000000 --- a/src/easyshapes_app/Gui/Pages/Toolbox/MainArea/GraphsView.qml +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-FileCopyrightText: 2024 EasyApp contributors -// SPDX-License-Identifier: BSD-3-Clause -// © 2024 Contributors to the EasyApp project - -import QtQuick -import QtGraphs - -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Elements as EaElements - -import Gui.Globals as Globals - - -GraphsView { - anchors.fill: parent - - marginTop: EaStyle.Sizes.fontPixelSize * 2 - marginBottom: EaStyle.Sizes.fontPixelSize * 2 - marginLeft: EaStyle.Sizes.fontPixelSize - marginRight: EaStyle.Sizes.fontPixelSize * 2 - - zoomAreaEnabled: true - - // theme - theme: GraphsTheme { - backgroundColor: EaStyle.Colors.chartBackground - plotAreaBackgroundColor: EaStyle.Colors.chartBackground - - axisX.mainColor: EaStyle.Colors.chartGridLine - axisX.mainWidth: 0 - - axisY.mainColor: EaStyle.Colors.chartGridLine - axisY.mainWidth: 0 - - gridVisible: true - grid.mainWidth: 1 - grid.subWidth: 0 - grid.mainColor: EaStyle.Colors.chartGridLine - grid.subColor: EaStyle.Colors.chartMinorGridLine - - labelFont.family: EaStyle.Fonts.fontFamily - labelFont.pixelSize: EaStyle.Sizes.fontPixelSize - labelTextColor: EaStyle.Colors.chartLabels - } - // theme - - // axisX - axisX: ValueAxis { - labelDelegate: TextEdit { - horizontalAlignment: TextInput.AlignHCenter - verticalAlignment: Text.AlignVCenter - bottomPadding: EaStyle.Sizes.fontPixelSize - color: EaStyle.Colors.chartLabels - } - - titleText: 'x' - min: 0 - max: 100 - } - // axisX - - // axisY - axisY: ValueAxis { - labelDelegate: TextEdit { - horizontalAlignment: TextInput.AlignRight - verticalAlignment: Text.AlignVCenter - rightPadding: -EaStyle.Sizes.fontPixelSize - color: EaStyle.Colors.chartLabels - } - - titleText: 'y' - min: -2 - max: 2 - } - // axisY - - // lineSeries - LineSeries { - color: 'red' - - XYPoint { x: 0; y: -1 } - XYPoint { x: 50; y: 1.5 } - XYPoint { x: 100; y: -0.5 } - } - // lineSeries - -} diff --git a/src/easyshapes_app/Gui/Pages/Toolbox/MainArea/Image.qml b/src/easyshapes_app/Gui/Pages/Toolbox/MainArea/Image.qml deleted file mode 100644 index fa89bc8..0000000 --- a/src/easyshapes_app/Gui/Pages/Toolbox/MainArea/Image.qml +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-FileCopyrightText: 2024 EasyApp contributors -// SPDX-License-Identifier: BSD-3-Clause -// © 2024 Contributors to the EasyApp project - -import QtQuick -import QtGraphs - -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Elements as EaElements - -import Gui.Globals as Globals - -Image { - - fillMode: Image.PreserveAspectFit - source: "../../../Resources/Images/structure.png" - -} diff --git a/src/easyshapes_app/Gui/Pages/Toolbox/MainArea/TextAreaPlain.qml b/src/easyshapes_app/Gui/Pages/Toolbox/MainArea/TextAreaPlain.qml deleted file mode 100644 index 9d1b2e8..0000000 --- a/src/easyshapes_app/Gui/Pages/Toolbox/MainArea/TextAreaPlain.qml +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-FileCopyrightText: 2024 EasyApp contributors -// SPDX-License-Identifier: BSD-3-Clause -// © 2024 Contributors to the EasyApp project - -import QtQuick -import QtGraphs - -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Elements as EaElements - -import Gui.Globals as Globals - - -EaElements.TextArea { - text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, -sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. -Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris -nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in -reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla -pariatur. Excepteur sint occaecat cupidatat non proident, sunt in -culpa qui officia deserunt mollit anim id est laborum.' -} diff --git a/src/easyshapes_app/Gui/Pages/Toolbox/MainArea/TextAreaRich.qml b/src/easyshapes_app/Gui/Pages/Toolbox/MainArea/TextAreaRich.qml deleted file mode 100644 index 7a04f57..0000000 --- a/src/easyshapes_app/Gui/Pages/Toolbox/MainArea/TextAreaRich.qml +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-FileCopyrightText: 2024 EasyApp contributors -// SPDX-License-Identifier: BSD-3-Clause -// © 2024 Contributors to the EasyApp project - -import QtQuick -import QtGraphs - -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Elements as EaElements - -import Gui.Globals as Globals - - -EaElements.TextArea { - textFormat: TextEdit.RichText - - text: '

- Lorem ipsum dolor sit amet, consectetur adipiscing elit, - sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. - Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris - nisi ut aliquip ex ea
commodo consequat. -

- -

- Duis aute irure dolor in - reprehenderit in voluptate velit esse cillum dolore eu fugiat - nulla pariatur.
- Excepteur sint occaecat cupidatat non proident, sunt in - culpa qui officia deserunt mollit anim id est laborum. -

' -} diff --git a/src/easyshapes_app/Gui/Pages/Toolbox/Sidebar/Basic/Groups/Group1.qml b/src/easyshapes_app/Gui/Pages/Toolbox/Sidebar/Basic/Groups/Group1.qml deleted file mode 100644 index f177a50..0000000 --- a/src/easyshapes_app/Gui/Pages/Toolbox/Sidebar/Basic/Groups/Group1.qml +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: 2024 EasyApp contributors -// SPDX-License-Identifier: BSD-3-Clause -// © 2024 Contributors to the EasyApp project - -import QtQuick -import QtQuick.Controls - -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents -import EasyApp.Gui.Logic as EaLogic - -import Gui.Globals as Globals - -EaElements.GroupColumn {} diff --git a/src/easyshapes_app/Gui/Pages/Toolbox/Sidebar/Basic/Groups/Group2.qml b/src/easyshapes_app/Gui/Pages/Toolbox/Sidebar/Basic/Groups/Group2.qml deleted file mode 100644 index 585ee24..0000000 --- a/src/easyshapes_app/Gui/Pages/Toolbox/Sidebar/Basic/Groups/Group2.qml +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-FileCopyrightText: 2024 EasyApp contributors -// SPDX-License-Identifier: BSD-3-Clause -// © 2024 Contributors to the EasyApp project - -import QtQuick -import QtQuick.Controls - -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents -import EasyApp.Gui.Logic as EaLogic - -import Gui.Globals as Globals - -EaElements.GroupColumn { - - EaElements.Label { - text: "EaElements.Label" - } - - EaElements.TextInput { - text: 'EaElements.TextInput' - } - - EaElements.TextField { - text: 'EaElements.TextField' - } - -} diff --git a/src/easyshapes_app/Gui/Pages/Toolbox/Sidebar/Basic/Groups/Group3.qml b/src/easyshapes_app/Gui/Pages/Toolbox/Sidebar/Basic/Groups/Group3.qml deleted file mode 100644 index f57f027..0000000 --- a/src/easyshapes_app/Gui/Pages/Toolbox/Sidebar/Basic/Groups/Group3.qml +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: 2024 EasyApp contributors -// SPDX-License-Identifier: BSD-3-Clause -// © 2024 Contributors to the EasyApp project - -import QtQuick -import QtQuick.Controls - -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents -import EasyApp.Gui.Logic as EaLogic - -import Gui.Globals as Globals - -EaElements.GroupColumn { - - EaElements.ComboBox { - model: ["EaElements.ComboBox 1", "EaElements.ComboBox 2", "EaElements.ComboBox 3"] - } - - Column { - EaElements.RadioButton { - checked: true - text: qsTr("EaElements.RadioButton 1") - } - EaElements.RadioButton { - text: qsTr("EaElements.RadioButton 2") - } - } - - Column { - spacing: 10 - EaElements.CheckBox { - checked: true - text: qsTr("EaElements.CheckBox 1") - } - EaElements.CheckBox { - text: qsTr("EaElements.CheckBox 2") - } - } - -} diff --git a/src/easyshapes_app/Gui/Pages/Toolbox/Sidebar/Basic/Groups/Group4.qml b/src/easyshapes_app/Gui/Pages/Toolbox/Sidebar/Basic/Groups/Group4.qml deleted file mode 100644 index a3d5b92..0000000 --- a/src/easyshapes_app/Gui/Pages/Toolbox/Sidebar/Basic/Groups/Group4.qml +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-FileCopyrightText: 2024 EasyApp contributors -// SPDX-License-Identifier: BSD-3-Clause -// © 2024 Contributors to the EasyApp project - -import QtQuick -import QtQuick.Controls - -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Style as EaStyle -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents -import EasyApp.Gui.Logic as EaLogic - -import Gui.Globals as Globals - -EaElements.GroupColumn { - - - EaElements.SideBarButton { - fontIcon: 'plus-circle' - text: 'EaElements.SideBarButton' - } - - EaElements.Slider { - from: 1 - value: 25 - to: 100 - } - -} diff --git a/src/easyshapes_app/Gui/Pages/Toolbox/Sidebar/Basic/Layout.qml b/src/easyshapes_app/Gui/Pages/Toolbox/Sidebar/Basic/Layout.qml deleted file mode 100644 index 5e16781..0000000 --- a/src/easyshapes_app/Gui/Pages/Toolbox/Sidebar/Basic/Layout.qml +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2024 EasyApp contributors -// SPDX-License-Identifier: BSD-3-Clause -// © 2024 Contributors to the EasyApp project - -import QtQuick -import QtQuick.Controls - -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents - -import Gui.Globals as Globals - - -EaComponents.SideBarColumn { - - EaElements.GroupBox { - title: qsTr('Group 1: Empty') - icon: 'rocket' - - Loader { source: 'Groups/Group1.qml' } - } - - EaElements.GroupBox { - title: qsTr('Group 2: Label, TextInput, TextField') - icon: 'rocket' - collapsed: false - - Loader { source: 'Groups/Group2.qml' } - } - - EaElements.GroupBox { - title: qsTr('Group 3: ComboBox, RadioButton, CheckBox') - icon: 'rocket' - - Loader { source: 'Groups/Group3.qml' } - } - - EaElements.GroupBox { - title: qsTr('Group 4: SidebarButton, Slider') - icon: 'rocket' - - Loader { source: 'Groups/Group4.qml' } - } - -} diff --git a/src/easyshapes_app/Gui/Resources/Images/scattering.jpg b/src/easyshapes_app/Gui/Resources/Images/scattering.jpg new file mode 100644 index 0000000..1ebed3f Binary files /dev/null and b/src/easyshapes_app/Gui/Resources/Images/scattering.jpg differ diff --git a/src/easyshapes_app/Gui/Resources/molecule.pdb b/src/easyshapes_app/Gui/Resources/molecule.pdb new file mode 100644 index 0000000..6897e62 --- /dev/null +++ b/src/easyshapes_app/Gui/Resources/molecule.pdb @@ -0,0 +1,135 @@ +REMARK +ATOM 1 N POPCA 1 -1.628 -1.762 11.597 1.00 0.00 A N +ATOM 2 C12 POPCA 1 -2.376 -3.129 11.317 1.00 0.00 A C +ATOM 3 H12A POPCA 1 -2.294 -3.725 12.214 1.00 0.00 A H +ATOM 4 H12B POPCA 1 -1.904 -3.650 10.498 1.00 0.00 A H +ATOM 5 C13 POPCA 1 -0.222 -2.018 11.969 1.00 0.00 A C +ATOM 6 H13A POPCA 1 -0.225 -2.752 12.761 1.00 0.00 A H +ATOM 7 H13B POPCA 1 0.347 -2.478 11.174 1.00 0.00 A H +ATOM 8 H13C POPCA 1 0.321 -1.190 12.400 1.00 0.00 A H +ATOM 9 C14 POPCA 1 -1.612 -0.790 10.423 1.00 0.00 A C +ATOM 10 H14A POPCA 1 -2.639 -0.829 10.093 1.00 0.00 A H +ATOM 11 H14B POPCA 1 -1.360 0.231 10.669 1.00 0.00 A H +ATOM 12 H14C POPCA 1 -0.912 -1.102 9.662 1.00 0.00 A H +ATOM 13 C15 POPCA 1 -2.242 -1.134 12.850 1.00 0.00 A C +ATOM 14 H15A POPCA 1 -1.837 -0.154 13.056 1.00 0.00 A H +ATOM 15 H15B POPCA 1 -2.091 -1.742 13.730 1.00 0.00 A H +ATOM 16 H15C POPCA 1 -3.302 -0.980 12.715 1.00 0.00 A H +ATOM 17 C11 POPCA 1 -3.887 -2.848 11.003 1.00 0.00 A C +ATOM 18 H11A POPCA 1 -4.232 -2.484 11.995 1.00 0.00 A H +ATOM 19 H11B POPCA 1 -4.385 -3.812 10.768 1.00 0.00 A H +ATOM 20 P POPCA 1 -5.348 -1.753 9.168 1.00 0.00 A P +ATOM 21 O13 POPCA 1 -5.595 -0.383 9.566 1.00 0.00 A O +ATOM 22 O14 POPCA 1 -6.302 -2.795 9.531 1.00 0.00 A O +ATOM 23 O12 POPCA 1 -3.970 -1.995 9.851 1.00 0.00 A O +ATOM 24 O11 POPCA 1 -4.981 -1.795 7.640 1.00 0.00 A O +ATOM 25 C1 POPCA 1 -3.932 -2.520 7.052 1.00 0.00 A C +ATOM 26 HA POPCA 1 -4.285 -2.725 6.018 1.00 0.00 A H +ATOM 27 HB POPCA 1 -3.682 -3.511 7.486 1.00 0.00 A H +ATOM 28 C2 POPCA 1 -2.623 -1.763 6.939 1.00 0.00 A C +ATOM 29 HS POPCA 1 -2.582 -1.208 7.900 1.00 0.00 A H +ATOM 30 O21 POPCA 1 -2.777 -0.772 5.947 1.00 0.00 A O +ATOM 31 C21 POPCA 1 -1.865 0.197 5.973 1.00 0.00 A C +ATOM 32 O22 POPCA 1 -0.973 0.362 6.813 1.00 0.00 A O +ATOM 33 C22 POPCA 1 -2.267 1.169 4.884 1.00 0.00 A C +ATOM 34 H2R POPCA 1 -3.173 1.730 5.200 1.00 0.00 A H +ATOM 35 H2S POPCA 1 -2.425 0.490 4.019 1.00 0.00 A H +ATOM 36 C3 POPCA 1 -1.452 -2.658 6.753 1.00 0.00 A C +ATOM 37 HX POPCA 1 -1.422 -3.386 7.592 1.00 0.00 A H +ATOM 38 HY POPCA 1 -0.496 -2.096 6.829 1.00 0.00 A H +ATOM 39 O31 POPCA 1 -1.540 -3.417 5.530 1.00 0.00 A O +ATOM 40 C31 POPCA 1 -0.769 -3.071 4.532 1.00 0.00 A C +ATOM 41 O32 POPCA 1 0.140 -2.241 4.567 1.00 0.00 A O +ATOM 42 C32 POPCA 1 -1.120 -3.860 3.307 1.00 0.00 A C +ATOM 43 H2X POPCA 1 -2.149 -4.280 3.317 1.00 0.00 A H +ATOM 44 H2Y POPCA 1 -0.534 -4.803 3.256 1.00 0.00 A H +ATOM 45 C23 POPCA 1 -1.275 2.333 4.574 1.00 0.00 A C +ATOM 46 H3R POPCA 1 -0.955 2.762 5.548 1.00 0.00 A H +ATOM 47 H3S POPCA 1 -1.895 3.064 4.013 1.00 0.00 A H +ATOM 48 C24 POPCA 1 -0.124 1.785 3.712 1.00 0.00 A C +ATOM 49 H4R POPCA 1 -0.559 1.116 2.939 1.00 0.00 A H +ATOM 50 H4S POPCA 1 0.599 1.205 4.325 1.00 0.00 A H +ATOM 51 C25 POPCA 1 0.703 2.846 3.019 1.00 0.00 A C +ATOM 52 H5R POPCA 1 0.961 3.668 3.721 1.00 0.00 A H +ATOM 53 H5S POPCA 1 0.098 3.364 2.245 1.00 0.00 A H +ATOM 54 C26 POPCA 1 2.011 2.311 2.397 1.00 0.00 A C +ATOM 55 H6R POPCA 1 1.861 1.508 1.645 1.00 0.00 A H +ATOM 56 H6S POPCA 1 2.727 1.959 3.171 1.00 0.00 A H +ATOM 57 C27 POPCA 1 2.556 3.461 1.488 1.00 0.00 A C +ATOM 58 H7R POPCA 1 2.957 4.238 2.174 1.00 0.00 A H +ATOM 59 H7S POPCA 1 1.730 3.855 0.859 1.00 0.00 A H +ATOM 60 C28 POPCA 1 3.645 2.891 0.549 1.00 0.00 A C +ATOM 61 H8R POPCA 1 3.113 2.216 -0.156 1.00 0.00 A H +ATOM 62 H8S POPCA 1 4.359 2.309 1.169 1.00 0.00 A H +ATOM 63 C29 POPCA 1 4.225 4.177 -0.063 1.00 0.00 A C +ATOM 64 H91 POPCA 1 4.684 4.845 0.680 1.00 0.00 A H +ATOM 65 C210 POPCA 1 4.285 4.458 -1.342 1.00 0.00 A C +ATOM 66 H101 POPCA 1 4.674 5.455 -1.597 1.00 0.00 A H +ATOM 67 C211 POPCA 1 3.750 3.728 -2.495 1.00 0.00 A C +ATOM 68 H11R POPCA 1 2.722 4.144 -2.563 1.00 0.00 A H +ATOM 69 H11S POPCA 1 3.712 2.629 -2.337 1.00 0.00 A H +ATOM 70 C212 POPCA 1 4.281 4.069 -3.940 1.00 0.00 A C +ATOM 71 H12R POPCA 1 4.220 5.160 -4.141 1.00 0.00 A H +ATOM 72 H12S POPCA 1 3.663 3.642 -4.758 1.00 0.00 A H +ATOM 73 C213 POPCA 1 5.721 3.534 -4.003 1.00 0.00 A C +ATOM 74 H13R POPCA 1 5.766 2.477 -3.665 1.00 0.00 A H +ATOM 75 H13S POPCA 1 6.405 4.132 -3.363 1.00 0.00 A H +ATOM 76 C214 POPCA 1 6.377 3.519 -5.402 1.00 0.00 A C +ATOM 77 H14R POPCA 1 6.042 2.618 -5.957 1.00 0.00 A H +ATOM 78 H14S POPCA 1 7.451 3.467 -5.120 1.00 0.00 A H +ATOM 79 C215 POPCA 1 6.143 4.755 -6.334 1.00 0.00 A C +ATOM 80 H15R POPCA 1 6.438 5.637 -5.726 1.00 0.00 A H +ATOM 81 H15S POPCA 1 5.058 4.771 -6.571 1.00 0.00 A H +ATOM 82 C216 POPCA 1 6.921 4.780 -7.565 1.00 0.00 A C +ATOM 83 H16R POPCA 1 7.960 4.556 -7.241 1.00 0.00 A H +ATOM 84 H16S POPCA 1 6.924 5.793 -8.021 1.00 0.00 A H +ATOM 85 C217 POPCA 1 6.396 3.829 -8.671 1.00 0.00 A C +ATOM 86 H17R POPCA 1 5.405 4.090 -9.100 1.00 0.00 A H +ATOM 87 H17S POPCA 1 6.183 2.845 -8.201 1.00 0.00 A H +ATOM 88 C218 POPCA 1 7.419 3.731 -9.870 1.00 0.00 A C +ATOM 89 H18R POPCA 1 7.080 2.929 -10.560 1.00 0.00 A H +ATOM 90 H18S POPCA 1 8.445 3.522 -9.496 1.00 0.00 A H +ATOM 91 H18T POPCA 1 7.406 4.715 -10.385 1.00 0.00 A H +ATOM 92 C33 POPCA 1 -0.830 -3.000 2.040 1.00 0.00 A C +ATOM 93 H3X POPCA 1 0.278 -2.920 2.035 1.00 0.00 A H +ATOM 94 H3Y POPCA 1 -1.222 -1.972 2.193 1.00 0.00 A H +ATOM 95 C34 POPCA 1 -1.345 -3.608 0.766 1.00 0.00 A C +ATOM 96 H4X POPCA 1 -2.431 -3.840 0.797 1.00 0.00 A H +ATOM 97 H4Y POPCA 1 -0.831 -4.573 0.570 1.00 0.00 A H +ATOM 98 C35 POPCA 1 -1.032 -2.798 -0.575 1.00 0.00 A C +ATOM 99 H5X POPCA 1 0.045 -2.530 -0.533 1.00 0.00 A H +ATOM 100 H5Y POPCA 1 -1.525 -1.802 -0.574 1.00 0.00 A H +ATOM 101 C36 POPCA 1 -1.505 -3.426 -1.911 1.00 0.00 A C +ATOM 102 H6X POPCA 1 -2.602 -3.530 -1.765 1.00 0.00 A H +ATOM 103 H6Y POPCA 1 -1.091 -4.447 -2.047 1.00 0.00 A H +ATOM 104 C37 POPCA 1 -1.115 -2.553 -3.161 1.00 0.00 A C +ATOM 105 H7X POPCA 1 -0.009 -2.544 -3.264 1.00 0.00 A H +ATOM 106 H7Y POPCA 1 -1.622 -1.566 -3.122 1.00 0.00 A H +ATOM 107 C38 POPCA 1 -1.761 -3.094 -4.443 1.00 0.00 A C +ATOM 108 H8X POPCA 1 -2.855 -3.216 -4.296 1.00 0.00 A H +ATOM 109 H8Y POPCA 1 -1.491 -4.163 -4.579 1.00 0.00 A H +ATOM 110 C39 POPCA 1 -1.349 -2.188 -5.648 1.00 0.00 A C +ATOM 111 H9X POPCA 1 -0.264 -2.213 -5.886 1.00 0.00 A H +ATOM 112 H9Y POPCA 1 -1.654 -1.168 -5.331 1.00 0.00 A H +ATOM 113 C310 POPCA 1 -2.148 -2.523 -6.913 1.00 0.00 A C +ATOM 114 H10X POPCA 1 -3.214 -2.565 -6.604 1.00 0.00 A H +ATOM 115 H10Y POPCA 1 -1.739 -3.469 -7.329 1.00 0.00 A H +ATOM 116 C311 POPCA 1 -1.814 -1.420 -7.906 1.00 0.00 A C +ATOM 117 H11X POPCA 1 -0.721 -1.244 -8.005 1.00 0.00 A H +ATOM 118 H11Y POPCA 1 -2.343 -0.478 -7.646 1.00 0.00 A H +ATOM 119 C312 POPCA 1 -2.433 -1.707 -9.284 1.00 0.00 A C +ATOM 120 H12X POPCA 1 -3.373 -2.283 -9.151 1.00 0.00 A H +ATOM 121 H12Y POPCA 1 -1.656 -2.272 -9.842 1.00 0.00 A H +ATOM 122 C313 POPCA 1 -2.615 -0.439 -10.124 1.00 0.00 A C +ATOM 123 H13X POPCA 1 -1.617 0.048 -10.151 1.00 0.00 A H +ATOM 124 H13Y POPCA 1 -3.499 0.070 -9.684 1.00 0.00 A H +ATOM 125 C314 POPCA 1 -2.874 -0.832 -11.560 1.00 0.00 A C +ATOM 126 H14X POPCA 1 -3.760 -1.477 -11.742 1.00 0.00 A H +ATOM 127 H14Y POPCA 1 -2.004 -1.385 -11.976 1.00 0.00 A H +ATOM 128 C315 POPCA 1 -3.135 0.378 -12.459 1.00 0.00 A C +ATOM 129 H15X POPCA 1 -2.252 1.042 -12.333 1.00 0.00 A H +ATOM 130 H15Y POPCA 1 -4.060 0.876 -12.098 1.00 0.00 A H +ATOM 131 C316 POPCA 1 -3.480 -0.027 -13.887 1.00 0.00 A C +ATOM 132 H16X POPCA 1 -4.277 -0.802 -13.882 1.00 0.00 A H +ATOM 133 H16Y POPCA 1 -2.605 -0.568 -14.306 1.00 0.00 A H +ATOM 134 H16Z POPCA 1 -3.860 0.816 -14.503 1.00 0.00 A H diff --git a/src/easyshapes_app/Gui/StatusBar.qml b/src/easyshapes_app/Gui/StatusBar.qml index 2bd91f9..0f39f5c 100644 --- a/src/easyshapes_app/Gui/StatusBar.qml +++ b/src/easyshapes_app/Gui/StatusBar.qml @@ -5,57 +5,57 @@ import QtQuick import QtQuick.Controls -import EasyApp.Gui.Globals as EaGlobals -import EasyApp.Gui.Elements as EaElements -import EasyApp.Gui.Components as EaComponents +import EasyApplication.Gui.Globals as EaGlobals +import EasyApplication.Gui.Elements as EaElements import Gui.Globals as Globals - EaElements.StatusBar { visible: EaGlobals.Vars.appBarCurrentIndex !== 0 EaElements.StatusBarItem { - keyIcon: 'archive' - keyText: qsTr('Project') + keyIcon: "archive" + keyText: qsTr("Project") valueText: Globals.BackendWrapper.statusProject - ToolTip.text: qsTr('Current project') + ToolTip.text: qsTr("Current project") } EaElements.StatusBarItem { - keyIcon: 'layer-group' - keyText: qsTr('Models') - valueText: Globals.BackendWrapper.statusPhasesCount - ToolTip.text: qsTr('Number of models added') + keyIcon: "vial" + keyText: qsTr("Shape") + // Bare shape for a discrete model, or "Lattice ()" on a lattice. + valueText: { + const shape = Globals.BackendWrapper.sampleModelCurrentStructureType + if (!shape) + return "" + return Globals.BackendWrapper.sampleModelCurrentType === "Lattice" + ? qsTr("Lattice (%1)").arg(shape.toLowerCase()) + : shape + } + ToolTip.text: qsTr("Current sample model shape and lattice arrangement") } EaElements.StatusBarItem { - keyIcon: 'microscope' - keyText: qsTr('Experiments') - valueText: Globals.BackendWrapper.statusExperimentsCount - ToolTip.text: qsTr('Number of experiments added') + keyIcon: "puzzle-piece" + keyText: qsTr("Components") + valueText: "" + (Globals.BackendWrapper.componentsLoaded + ? Globals.BackendWrapper.componentsLoaded.count + : 0) + ToolTip.text: qsTr("Number of components") } EaElements.StatusBarItem { - keyIcon: 'calculator' - keyText: qsTr('Calculator') - valueText: Globals.BackendWrapper.statusCalculator - ToolTip.text: qsTr('Current calculation engine') + keyIcon: "cogs" + keyText: qsTr("Engine") + valueText: Globals.BackendWrapper.statusEngine + ToolTip.text: qsTr("Simulation engine") } EaElements.StatusBarItem { - keyIcon: 'level-down-alt' - keyText: qsTr('Minimizer') - valueText: Globals.BackendWrapper.statusMinimizer - ToolTip.text: qsTr('Current minimization engine and method') + keyIcon: "bezier-curve" + keyText: qsTr("Force field") + valueText: Globals.BackendWrapper.analysisConfigForceField + ToolTip.text: qsTr("Force field selected on the Analysis page") } - - EaElements.StatusBarItem { - keyIcon: 'th-list' - keyText: qsTr('Parameters') - valueText: Globals.BackendWrapper.statusVariables - ToolTip.text: qsTr('Number of parameters: total, free and fixed') - } - } diff --git a/src/easyshapes_app/main.cpp b/src/easyshapes_app/main.cpp new file mode 100644 index 0000000..098404e --- /dev/null +++ b/src/easyshapes_app/main.cpp @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2021-2026 EasyPeasy contributors +// SPDX-License-Identifier: BSD-3-Clause + +#include +#include +#include +#include + +#ifdef EASYSHAPES_WEBENGINE +#include +#endif + + +int main(int argc, char *argv[]) +{ +#ifdef EASYSHAPES_WEBENGINE + // Must run before the QGuiApplication is constructed. Only defined for + // desktop builds configured with CONFIG+=webengine - see the .pro file. + QtWebEngineQuick::initialize(); +#endif + + // Qt Quick clips with the stencil buffer whenever a scissor rectangle is + // not enough, for instance inside a transform. WebGL gives no stencil + // attachment unless one is requested, and clipping then fails silently: + // the collapsed EaElements.GroupBox draws its content outside its own + // bounds. Must be set before the application is constructed. + QSurfaceFormat format = QSurfaceFormat::defaultFormat(); + format.setStencilBufferSize(8); + format.setDepthBufferSize(24); + QSurfaceFormat::setDefaultFormat(format); + + // Create Qt application + QGuiApplication app(argc, argv); + + // Needed by the QML Settings elements (EasyApplication Vars/Colors/ + // PreferencesDialog). Without these, Settings warns and never persists. + // On WebAssembly the values go to the browser IndexedDB store. + QGuiApplication::setOrganizationName("EasyScience"); + QGuiApplication::setOrganizationDomain("easyscience.software"); + QGuiApplication::setApplicationName("EasyShapes"); + + // Title bar and taskbar icon. Nothing in the QML sets one. PNG rather + // than SVG, so that no QtSvg image plugin has to be linked in. + // Has no effect on WebAssembly, where there is no window decoration. + QGuiApplication::setWindowIcon(QIcon(":/Gui/Resources/Logos/App.png")); + + // Create the QML application engine + QQmlApplicationEngine engine; + + // Add the paths where QML searches for components. + // Everything is served from the compiled-in resources, since the browser + // has no file system. Aliases in easyshapes_app.qrc map both the app's + // own QML (Gui, Backends) and the EasyApplication modules under 'qrc:/'. + engine.addImportPath("qrc:/"); + + // Load the main QML component + engine.load("qrc:/main.qml"); + + // Start the application event loop + if (engine.rootObjects().isEmpty()) + return -1; + return app.exec(); +} diff --git a/src/easyshapes_app/test.qml b/src/easyshapes_app/test.qml new file mode 100644 index 0000000..cb4d953 --- /dev/null +++ b/src/easyshapes_app/test.qml @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +import EasyApplication.Gui.Globals as EaGlobals +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Components as EaComponents +import EasyApplication.Gui.Elements as EaElements +import Qt.labs.qmlmodels + +import Gui.Globals as Globals + +ApplicationWindow{ + + width: 650 + height: 680 + color: EaStyle.Colors.contentBackground + visible: true + + Grid { + spacing: EaStyle.Sizes.fontPixelSize * 4 + leftPadding: 12 + + Column { + width: 600 + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.ComboBox { + model: [qsTr("Light"), qsTr("Dark"), qsTr("System")] + onActivated: { + if (currentIndex === 0) + EaStyle.Colors.theme = EaStyle.Colors.LightTheme + else if (currentIndex === 1) + EaStyle.Colors.theme = EaStyle.Colors.DarkTheme + else if (currentIndex === 2) + EaStyle.Colors.theme = EaStyle.Colors.SystemTheme + } + Component.onCompleted: { + if (EaStyle.Colors.theme === EaStyle.Colors.LightTheme) + currentIndex = 0 + else if (EaStyle.Colors.theme === EaStyle.Colors.DarkTheme) + currentIndex = 1 + else if (EaStyle.Colors.theme === EaStyle.Colors.SystemTheme) + currentIndex = 2 + } + } + + // groubox to test the element + // EaElements.GroupBox { + // title: qsTr('Test a group widget') + // icon: 'wrench' + // collapsed: false + + // Loader { source: 'Gui/Pages/SampleModel/Sidebar/Basic/Groups/Solution.qml'} + // } + + EaElements.Pill { + text: "text" + } + + EaElements.Pill { + text: "fontIcon" + fontIcon: "atom" + } + + EaElements.Pill { + text: "superlongtext why can't I hold all this text in my hands" + } + } + } + + + + //Component.onCompleted: Globals.References.pages.samplemodel.sidebar.basic.popups.LoadExistingModel.open() + +} diff --git a/src/easyshapes_app/test2.qml b/src/easyshapes_app/test2.qml new file mode 100644 index 0000000..4468ba5 --- /dev/null +++ b/src/easyshapes_app/test2.qml @@ -0,0 +1,166 @@ +// SPDX-FileCopyrightText: 2024 EasyApp contributors +// SPDX-License-Identifier: BSD-3-Clause +// © 2024 Contributors to the EasyApp project + +import QtQuick +import QtQuick.Controls +import QtQuick.Dialogs + +import EasyApplication.Gui.Globals as EaGlobals +import EasyApplication.Gui.Style as EaStyle +import EasyApplication.Gui.Components as EaComponents +import EasyApplication.Gui.Elements as EaElements +import Qt.labs.qmlmodels + +import Gui.Globals as Globals + +ApplicationWindow{ + + width: 650 + height: 580 + color: EaStyle.Colors.contentBackground + visible: true + + Grid { + spacing: EaStyle.Sizes.fontPixelSize * 4 + leftPadding: 12 + + + Column { + width: 600 + spacing: EaStyle.Sizes.fontPixelSize + + EaElements.ComboBox { + model: [qsTr("Light"), qsTr("Dark"), qsTr("System")] + onActivated: { + if (currentIndex === 0) + EaStyle.Colors.theme = EaStyle.Colors.LightTheme + else if (currentIndex === 1) + EaStyle.Colors.theme = EaStyle.Colors.DarkTheme + else if (currentIndex === 2) + EaStyle.Colors.theme = EaStyle.Colors.SystemTheme + } + Component.onCompleted: { + if (EaStyle.Colors.theme === EaStyle.Colors.LightTheme) + currentIndex = 0 + else if (EaStyle.Colors.theme === EaStyle.Colors.DarkTheme) + currentIndex = 1 + else if (EaStyle.Colors.theme === EaStyle.Colors.SystemTheme) + currentIndex = 2 + } + } + + EaComponents.TableView { + id: whatverTable + defaultInfoText: qsTr("No models found") + enabled: true + + header: EaComponents.TableViewHeader { + EaComponents.TableViewLabel { + id: modelNameColumnName + width: whatverTable.width * 0.25 + text: qsTr("Name") + color: EaStyle.Colors.themeForegroundMinor + leftPadding: EaStyle.Sizes.fontPixelSize * 0.7 + } + + EaComponents.TableViewLabel { + id: modelTypeColumnName + width: whatverTable.width * 0.15 + text: qsTr("Type") + color: EaStyle.Colors.themeForegroundMinor + } + + EaComponents.TableViewLabel { + id: modelDescrColumnName + width: whatverTable.width * 0.6 + text: qsTr("Description") + color: EaStyle.Colors.themeForegroundMinor + } + } + + model: ListModel { + id: availableSambleModelsModel + ListElement { name: "Samle1_aluv"; structure_type: "Vesicle"; description: "In order to avoid a prolonged pro-inflammatory neutrophil response, signaling downstream of an agonist-activated G protein-coupled receptor (GPCR) has to be rapidly terminated. Among the family of GPCR kinases (GRKs) that regulate receptor phosphorylation and signaling termination, GRK2, which is highly expressed by immune cells, plays an important role." } + ListElement { name: "Sample2_nanodisc"; structure_type: "Ring"; description: "The medium chain fatty acid receptor GPR84 as well as formyl peptide receptor 2 (FPR2)" } + ListElement { name: "Sample3_cubosome"; structure_type: "Lattice"; description: "receptors expressed in neutrophils, play a key role in regulating inflammation. In this study, we investigated the effects of GRK2 inhibitors on neutrophil functions induced by GPR84 and FPR2 agonists." } + ListElement { name: "Sample4"; structure_type: "Ring"; description: "GRK2 was shown to be expressed in human neutrophils and analysis of subcellular fractions" } + ListElement { name: "Sample5"; structure_type: "Ball"; description: "revealed a cytosolic localization. The GRK2 inhibitors enhanced and prolonged neutrophil production " } + ListElement { name: "Sample6"; structure_type: "Vesicle"; description: "production of reactive oxygen species (ROS) induced by GPR84- but not FPR2-agonists" } + ListElement { name: "Sample7"; structure_type: "Rod"; description: "suggesting a receptor selective function of GRK2. This suggestion was supported by β-arrestin recruitment data. The ROS production induced by a non β-arrestin recruiting GPR84" } + ListElement { name: "Sample8"; structure_type: "Bilayer"; description: "This suggestion was supported by β-arrestin recruitment data. The ROS production induced by a non β-arrestin recruiting GPR84 agonist was not affected by the GRK2 inhibitor." } + ListElement { name: "Sample9"; structure_type: "Monolayer"; description: "Termination of this β-arrestin independent response relied, similar to the response induced by FPR2 agonists, primarily on the actin cytoskeleton." } + ListElement { name: "Samplewithareallylongname"; structure_type: "Lattice"; description: "In summary, we show that GPR84 utilizes GRK2 in concert with β-arrestin and actin cytoskeleton dependent processes to fine-tune the activity of the ROS generating NADPH-oxidase in neutrophils." } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "1"; structure_type: "2"; description: "3" } + ListElement { name: "end1"; structure_type: "end2"; description: "end3" } + } + + delegate: EaComponents.TableViewDelegate { + required property int index + required property string name + required property string structure_type + required property string description + + EaComponents.TableViewLabel { + id: modelNameColumn + //width: whatverTable.width * 0.25 + text: name + } + + EaComponents.TableViewLabel { + id: typeColumn + //width: whatverTable.width * 0.15 + text: structure_type + } + + EaComponents.TableViewTextInput { + id: descrColumn + //width: whatverTable.width * 0.58 + text: description + } + } + + // ScrollBar.vertical: EaElements.ScrollBar { + // policy: ScrollBar.AlwaysOn // ScrollBar.AsNeeded // AlwaysOn + // } + + + } + } + } + + + + //Component.onCompleted: Globals.References.pages.samplemodel.sidebar.basic.popups.LoadExistingModel.open() + +}