Compare commits
22 Commits
alan/test-ci
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 237381939f | |||
| 6a8b2d9e28 | |||
| 54241d427e | |||
| 600c4460c1 | |||
| 8c49ff8ee0 | |||
| 5bd67fa171 | |||
| 0f5957cbd0 | |||
| 6cd0d47dae | |||
| 72e6566334 | |||
| 19b87984e4 | |||
| c694cf57a9 | |||
| 2f54e90cdf | |||
| 65a36557be | |||
| b635cbde8a | |||
| df710deefe | |||
| 318daf7b48 | |||
| 1f02c33c5b | |||
| aa66061925 | |||
| 648fb31558 | |||
| 5e8d2ee3ca | |||
| d20ecd982c | |||
| d319efba87 |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"gitversion.tool": {
|
||||
"version": "6.1.0",
|
||||
"commands": [
|
||||
"dotnet-gitversion"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
name: build_qgis
|
||||
on:
|
||||
push:
|
||||
branches: ["main", "installer-test/**"] # Continuous delivery on every long-lived branch
|
||||
tags: ["v3.*.*"] # Manual delivery on every v3.x tag
|
||||
|
||||
env:
|
||||
ZipName: "qgis.zip"
|
||||
|
||||
jobs:
|
||||
build-connector:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
semver: ${{ steps.set-version.outputs.semver }}
|
||||
file-version: ${{ steps.set-info-version.outputs.file-version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # fetch the whole repo history
|
||||
|
||||
- name: ⚒️ Run GitVersion
|
||||
run: ./build.sh build-server-version
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11" # pb_tool depends on distutils which was deprecated in 3.10 and removed in 3.12
|
||||
|
||||
- name: Setup Poetry Env
|
||||
run: |
|
||||
pip install poetry
|
||||
poetry self add poetry-plugin-export
|
||||
poetry install
|
||||
|
||||
- name: Export requirements.txt
|
||||
run: |
|
||||
poetry export --without-hashes --without-urls --output plugin_utils/requirements.txt
|
||||
|
||||
- name: Clean requirements.txt
|
||||
run: |
|
||||
python plugin_utils/patch_requirements.py
|
||||
|
||||
- name: Update version in plugin metadata.txt
|
||||
run: |
|
||||
python plugin_utils/patch_version.py ${{ env.GitVersion_FullSemVer }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install pb_tool==3.1.0 pyqt5==5.15.9
|
||||
|
||||
- name: ZIP plugin
|
||||
run: |
|
||||
pb_tool zip
|
||||
mv "zip_build/speckle-qgis-v3.zip" ${{env.ZipName}}
|
||||
|
||||
- id: set-version
|
||||
name: Set version to output
|
||||
run: echo "semver=${{ env.GitVersion_FullSemVer }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- id: set-info-version
|
||||
name: Set file version to output
|
||||
run: echo "file-version=${{ env.GitVersion_AssemblySemVer}}" >> "$GITHUB_OUTPUT" # version will be retrieved from tag?
|
||||
|
||||
- name: ⬆️ Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: output-${{ env.GitVersion_FullSemVer }}
|
||||
path: ${{env.ZipName}}
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
compression-level: 0 # no compression
|
||||
|
||||
deploy-installers:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-connector
|
||||
env:
|
||||
IS_PUBLIC_RELEASE: ${{ github.ref_type == 'tag' }}
|
||||
steps:
|
||||
- name: 🔫 Trigger Build Installer(s)
|
||||
uses: the-actions-org/workflow-dispatch@v4.0.0
|
||||
with:
|
||||
workflow: Build Installers
|
||||
repo: specklesystems/connector-installers
|
||||
token: ${{ secrets.CONNECTORS_GH_TOKEN }}
|
||||
inputs: '{
|
||||
"run_id": "${{ github.run_id }}",
|
||||
"semver": "${{ needs.build-connector.outputs.semver }}",
|
||||
"file_version": "${{ needs.build-connector.outputs.file-version }}",
|
||||
"repo": "${{ github.repository }}",
|
||||
"is_public_release": ${{ env.IS_PUBLIC_RELEASE }}
|
||||
}'
|
||||
ref: main
|
||||
wait-for-completion: true
|
||||
wait-for-completion-interval: 10s
|
||||
wait-for-completion-timeout: 10m
|
||||
display-workflow-run-url: true
|
||||
display-workflow-run-url-interval: 10s
|
||||
|
||||
- uses: geekyeggo/delete-artifact@v5
|
||||
with:
|
||||
name: output-*
|
||||
+27
@@ -121,6 +121,33 @@ zip-build*
|
||||
*.shp
|
||||
*.shx
|
||||
|
||||
# C#
|
||||
**/bin/*
|
||||
**/obj/*
|
||||
_ReSharper.SharpCompress/
|
||||
bin/
|
||||
*.suo
|
||||
*.user
|
||||
TestArchives/Scratch/
|
||||
TestArchives/Scratch2/
|
||||
TestResults/
|
||||
*.nupkg
|
||||
packages/*/
|
||||
project.lock.json
|
||||
tests/TestArchives/Scratch
|
||||
.vs
|
||||
tools
|
||||
.vscode
|
||||
.idea/
|
||||
|
||||
.DS_Store
|
||||
*.snupkg
|
||||
coverage.xml
|
||||
|
||||
*.received.*
|
||||
*.log
|
||||
|
||||
|
||||
/typings/*
|
||||
*.csv
|
||||
*/PyQt-UI*
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
workflow: GitFlow/v1
|
||||
next-version: 3.0.0
|
||||
branches:
|
||||
release:
|
||||
prevent-increment:
|
||||
when-current-commit-tagged: true
|
||||
+1
-21
@@ -11,8 +11,6 @@ try:
|
||||
from plugin_utils.installer import ensure_dependencies, startDebugger
|
||||
from plugin_utils.panel_logging import logger
|
||||
|
||||
from qgis.core import Qgis
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
def classFactory(iface): # pylint: disable=invalid-name
|
||||
"""Load SpeckleQGIS class from file SpeckleQGIS.
|
||||
@@ -27,29 +25,11 @@ try:
|
||||
|
||||
# Ensure dependencies are installed in the machine
|
||||
startDebugger()
|
||||
ensure_dependencies("QGIS")
|
||||
ensure_dependencies("QGISv3")
|
||||
|
||||
from speckle_qgis_v3 import SpeckleQGIS
|
||||
from specklepy.logging import metrics
|
||||
|
||||
version = (
|
||||
Qgis.QGIS_VERSION.encode("iso-8859-1", errors="ignore")
|
||||
.decode("utf-8")
|
||||
.split(".")[0]
|
||||
)
|
||||
metrics.set_host_app("qgis", version)
|
||||
return SpeckleQGIS(iface)
|
||||
|
||||
class EmptyClass:
|
||||
# https://docs.qgis.org/3.28/en/docs/pyqgis_developer_cookbook/plugins/plugins.html#mainplugin-py
|
||||
def __init__(self, iface):
|
||||
pass
|
||||
|
||||
def initGui(self):
|
||||
pass
|
||||
|
||||
def unload(self):
|
||||
pass
|
||||
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
$ErrorActionPreference = "Stop";
|
||||
|
||||
dotnet run --project ci-build/build.csproj -- $args
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
dotnet run --project ci-build/build.csproj -- "$@"
|
||||
@@ -0,0 +1,25 @@
|
||||
using static Bullseye.Targets;
|
||||
using static SimpleExec.Command;
|
||||
|
||||
const string RESTORE_TOOLS = "restore-tools";
|
||||
const string BUILD_SERVER_VERSION = "build-server-version";
|
||||
|
||||
Target(
|
||||
RESTORE_TOOLS,
|
||||
() =>
|
||||
{
|
||||
Run("dotnet", "tool restore");
|
||||
}
|
||||
);
|
||||
|
||||
Target(
|
||||
BUILD_SERVER_VERSION,
|
||||
DependsOn(RESTORE_TOOLS),
|
||||
() =>
|
||||
{
|
||||
Run("dotnet", "tool run dotnet-gitversion /output json /output buildserver");
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
await RunTargetsAndExitAsync(args).ConfigureAwait(true);
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bullseye" Version="5.0.0" />
|
||||
<PackageReference Include="Glob" Version="1.1.9"/>
|
||||
<PackageReference Include="Microsoft.Build" Version="17.10.4"/>
|
||||
<PackageReference Include="SimpleExec" Version="12.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="..\.github\workflows\release.yml">
|
||||
<Link>workflows\release.yml</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net8.0": {
|
||||
"Bullseye": {
|
||||
"type": "Direct",
|
||||
"requested": "[5.0.0, )",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "bqyt+m17ym+5aN45C5oZRAjuLDt8jKiCm/ys1XfymIXSkrTFwvI/QsbY3ucPSHDz7SF7uON7B57kXFv5H2k1ew=="
|
||||
},
|
||||
"Glob": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.1.9, )",
|
||||
"resolved": "1.1.9",
|
||||
"contentHash": "AfK5+ECWYTP7G3AAdnU8IfVj+QpGjrh9GC2mpdcJzCvtQ4pnerAGwHsxJ9D4/RnhDUz2DSzd951O/lQjQby2Sw=="
|
||||
},
|
||||
"Microsoft.Build": {
|
||||
"type": "Direct",
|
||||
"requested": "[17.10.4, )",
|
||||
"resolved": "17.10.4",
|
||||
"contentHash": "ZmGA8vhVXFzC4oo48ybQKlEybVKd0Ntfdr+Enqrn5ES1R6e/krIK9hLk0W33xuT0/G6QYd3YdhJZh+Xle717Ag==",
|
||||
"dependencies": {
|
||||
"Microsoft.Build.Framework": "17.10.4",
|
||||
"Microsoft.NET.StringTools": "17.10.4",
|
||||
"System.Collections.Immutable": "8.0.0",
|
||||
"System.Configuration.ConfigurationManager": "8.0.0",
|
||||
"System.Reflection.Metadata": "8.0.0",
|
||||
"System.Reflection.MetadataLoadContext": "8.0.0",
|
||||
"System.Security.Principal.Windows": "5.0.0",
|
||||
"System.Threading.Tasks.Dataflow": "8.0.0"
|
||||
}
|
||||
},
|
||||
"SimpleExec": {
|
||||
"type": "Direct",
|
||||
"requested": "[12.0.0, )",
|
||||
"resolved": "12.0.0",
|
||||
"contentHash": "ptxlWtxC8vM6Y6e3h9ZTxBBkOWnWrm/Sa1HT+2i1xcXY3Hx2hmKDZP5RShPf8Xr9D+ivlrXNy57ktzyH8kyt+Q=="
|
||||
},
|
||||
"Microsoft.Build.Framework": {
|
||||
"type": "Transitive",
|
||||
"resolved": "17.10.4",
|
||||
"contentHash": "4qXCwNOXBR1dyCzuks9SwTwFJQO/xmf2wcMislotDWJu7MN/r3xDNoU8Ae5QmKIHPaLG1xmfDkYS7qBVzxmeKw=="
|
||||
},
|
||||
"Microsoft.NET.StringTools": {
|
||||
"type": "Transitive",
|
||||
"resolved": "17.10.4",
|
||||
"contentHash": "wyABaqY+IHCMMSTQmcc3Ca6vbmg5BaEPgicnEgpll+4xyWZWlkQqUwafweUd9VAhBb4jqplMl6voUHQ6yfdUcg=="
|
||||
},
|
||||
"System.Collections.Immutable": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg=="
|
||||
},
|
||||
"System.Configuration.ConfigurationManager": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "JlYi9XVvIREURRUlGMr1F6vOFLk7YSY4p1vHo4kX3tQ0AGrjqlRWHDi66ImHhy6qwXBG3BJ6Y1QlYQ+Qz6Xgww==",
|
||||
"dependencies": {
|
||||
"System.Diagnostics.EventLog": "8.0.0",
|
||||
"System.Security.Cryptography.ProtectedData": "8.0.0"
|
||||
}
|
||||
},
|
||||
"System.Diagnostics.EventLog": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A=="
|
||||
},
|
||||
"System.Reflection.Metadata": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==",
|
||||
"dependencies": {
|
||||
"System.Collections.Immutable": "8.0.0"
|
||||
}
|
||||
},
|
||||
"System.Reflection.MetadataLoadContext": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "SZxrQ4sQYnIcdwiO3G/lHZopbPYQ2lW0ioT4JezgccWUrKaKbHLJbAGZaDfkYjWcta1pWssAo3MOXLsR0ie4tQ==",
|
||||
"dependencies": {
|
||||
"System.Collections.Immutable": "8.0.0",
|
||||
"System.Reflection.Metadata": "8.0.0"
|
||||
}
|
||||
},
|
||||
"System.Security.Cryptography.ProtectedData": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "+TUFINV2q2ifyXauQXRwy4CiBhqvDEDZeVJU7qfxya4aRYOKzVBpN+4acx25VcPB9ywUN6C0n8drWl110PhZEg=="
|
||||
},
|
||||
"System.Security.Principal.Windows": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA=="
|
||||
},
|
||||
"System.Threading.Tasks.Dataflow": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "7V0I8tPa9V7UxMx/+7DIwkhls5ouaEMQx6l/GwGm1Y8kJQ61On9B/PxCXFLbgu5/C47g0BP2CUYs+nMv1+Oaqw=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def patch_installer(tag):
|
||||
"""Patches the installer with the correct connector version and specklepy version"""
|
||||
iss_file = "speckle-sharp-ci-tools/qgis.iss"
|
||||
metadata = "metadata.txt"
|
||||
plugin_start_file = "speckle_qgis.py"
|
||||
|
||||
try:
|
||||
with open(iss_file, "r") as file:
|
||||
lines = file.readlines()
|
||||
new_lines = []
|
||||
for i, line in enumerate(lines):
|
||||
if "#define AppVersion " in line:
|
||||
line = f'#define AppVersion "{tag.split("-")[0]}"\n'
|
||||
if "#define AppInfoVersion " in line:
|
||||
line = f'#define AppInfoVersion "{tag}"\n'
|
||||
new_lines.append(line)
|
||||
with open(iss_file, "w") as file:
|
||||
file.writelines(new_lines)
|
||||
print(f"Patched installer with connector v{tag} ")
|
||||
file.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
with open(metadata, "r") as file:
|
||||
lines = file.readlines()
|
||||
new_lines = []
|
||||
for i, line in enumerate(lines):
|
||||
if "version=" in line:
|
||||
line = f"version={tag}\n" # .split("-")[0]
|
||||
if "experimental=" in line:
|
||||
if "-" in tag:
|
||||
line = f"experimental=True\n" # .split("-")[0]
|
||||
elif len(tag.split(".")) == 3 and tag != "0.0.99":
|
||||
line = f"experimental=False\n" # .split("-")[0]
|
||||
new_lines.append(line)
|
||||
with open(metadata, "w") as file:
|
||||
file.writelines(new_lines)
|
||||
print(f"Patched metadata v{tag} ")
|
||||
file.close()
|
||||
|
||||
with open(plugin_start_file, "r") as file:
|
||||
lines = file.readlines()
|
||||
for i, line in enumerate(lines):
|
||||
if "self.version = " in line:
|
||||
lines[i] = (
|
||||
lines[i].split('"')[0]
|
||||
+ '"'
|
||||
+ tag.split("-")[0]
|
||||
+ '"'
|
||||
+ lines[i].split('"')[2]
|
||||
)
|
||||
break
|
||||
with open(plugin_start_file, "w") as file:
|
||||
file.writelines(lines)
|
||||
print(f"Patched GIS start file with connector v{tag} and specklepy ")
|
||||
file.close()
|
||||
|
||||
r"""
|
||||
def whlFileRename(fileName: str):
|
||||
with open(fileName, "r") as file:
|
||||
lines = file.readlines()
|
||||
for i, line in enumerate(lines):
|
||||
if "-py3-none-any.whl" in line:
|
||||
p1 = line.split("-py3-none-any.whl")[0].split("-")[0]
|
||||
p2 = f'{tag.split("-")[0]}'
|
||||
p3 = line.split("-py3-none-any.whl")[1]
|
||||
lines[i] = p1+"-"+p2+"-py3-none-any.whl"+p3
|
||||
with open(fileName, "w") as file:
|
||||
file.writelines(lines)
|
||||
print(f"Patched toolbox_installer with connector v{tag} and specklepy ")
|
||||
file.close()
|
||||
|
||||
whlFileRename(conda_file)
|
||||
whlFileRename(toolbox_install_file)
|
||||
whlFileRename(toolbox_manual_install_file)
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
return
|
||||
|
||||
tag = sys.argv[1]
|
||||
if not re.match(r"([0-9]+)\.([0-9]+)\.([0-9]+)", tag):
|
||||
raise ValueError(f"Invalid tag provided: {tag}")
|
||||
|
||||
print(f"Patching version: {tag}")
|
||||
# patch_connector(tag.split("-")[0]) if I need to edit a connector file
|
||||
patch_installer(tag)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -186,6 +186,7 @@ def install_requirements(host_application: str) -> None:
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--pre",
|
||||
"-t",
|
||||
str(path),
|
||||
"-r",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
def main():
|
||||
"""Removes Python version and OS from Requirements.txt"""
|
||||
req_file = "plugin_utils/requirements.txt"
|
||||
|
||||
with open(req_file, "r") as file:
|
||||
lines = file.readlines()
|
||||
new_lines = []
|
||||
for i, line in enumerate(lines):
|
||||
new_line = line.split(";")[0].replace(" ", "")
|
||||
if "[" in new_line and "]" in new_line:
|
||||
new_line = new_line.split("[")[0] + new_line.split("]")[1]
|
||||
if i < len(lines) - 1:
|
||||
new_line += "\n"
|
||||
|
||||
new_lines.append(new_line)
|
||||
|
||||
with open(req_file, "w") as file:
|
||||
file.writelines(new_lines)
|
||||
print("Requirements file overwritten")
|
||||
file.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,39 @@
|
||||
import sys
|
||||
|
||||
|
||||
def patch_installer(tag):
|
||||
"""Patches the installer with the correct connector version and specklepy version"""
|
||||
|
||||
metadata = "metadata.txt"
|
||||
|
||||
with open(metadata, "r") as file:
|
||||
lines = file.readlines()
|
||||
new_lines = []
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if "version=" in line:
|
||||
line = f"version={tag}\n"
|
||||
if "experimental=" in line:
|
||||
if "-" in tag:
|
||||
line = f"experimental=True\n"
|
||||
elif len(tag.split(".")) == 3 and tag != "0.0.99" and "-" not in tag:
|
||||
line = f"experimental=False\n" # .split("-")[0]
|
||||
new_lines.append(line)
|
||||
|
||||
with open(metadata, "w") as file:
|
||||
file.writelines(new_lines)
|
||||
print(f"Patched metadata v{tag} ")
|
||||
file.close()
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
return
|
||||
|
||||
tag = sys.argv[1]
|
||||
print(f"Patching version: {tag}")
|
||||
patch_installer(tag)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,51 +1,51 @@
|
||||
annotated-types==0.7.0
|
||||
anyio==4.6.2.post1
|
||||
anyio==4.9.0
|
||||
appdirs==1.4.4
|
||||
attrs==23.2.0
|
||||
attrs==25.3.0
|
||||
backoff==2.2.1
|
||||
certifi==2024.8.30
|
||||
charset-normalizer==3.4.0
|
||||
certifi==2025.4.26
|
||||
charset-normalizer==3.4.2
|
||||
click-plugins==1.1.1
|
||||
click==8.1.7
|
||||
click==8.2.0
|
||||
cligj==0.7.2
|
||||
colorama==0.4.6
|
||||
deprecated==1.2.14
|
||||
deprecated==1.2.18
|
||||
earcut==1.1.5
|
||||
exceptiongroup==1.2.2
|
||||
exceptiongroup==1.3.0
|
||||
fiona==1.10.1
|
||||
geopandas==0.13.2
|
||||
geovoronoi==0.4.0
|
||||
gql==3.5.0
|
||||
graphql-core==3.2.5
|
||||
h11==0.14.0
|
||||
httpcore==1.0.6
|
||||
httpx==0.25.2
|
||||
gql==3.5.2
|
||||
graphql-core==3.2.4
|
||||
h11==0.16.0
|
||||
httpcore==1.0.9
|
||||
httpx==0.28.1
|
||||
idna==3.10
|
||||
importlib-metadata==8.5.0
|
||||
multidict==6.1.0
|
||||
multidict==6.4.3
|
||||
numpy==1.26.4
|
||||
packaging==24.1
|
||||
packaging==25.0
|
||||
pandas==2.2.3
|
||||
propcache==0.2.0
|
||||
pydantic-core==2.23.4
|
||||
pydantic==2.9.2
|
||||
pyproj==3.6.1
|
||||
propcache==0.3.1
|
||||
pydantic-core==2.33.2
|
||||
pydantic-settings==2.9.1
|
||||
pydantic==2.11.4
|
||||
pyproj==3.7.1
|
||||
pyshp==2.3.1
|
||||
python-dateutil==2.9.0.post0
|
||||
pytz==2024.2
|
||||
python-dotenv==1.1.0
|
||||
pytz==2025.2
|
||||
requests-toolbelt==1.0.0
|
||||
requests==2.31.0
|
||||
scipy==1.13.1
|
||||
shapely==2.0.6
|
||||
six==1.16.0
|
||||
scipy==1.15.3
|
||||
shapely==2.1.0
|
||||
six==1.17.0
|
||||
sniffio==1.3.1
|
||||
specklepy==2.21.3
|
||||
stringcase==1.2.0
|
||||
typing-extensions==4.12.2
|
||||
tzdata==2024.2
|
||||
specklepy==3.0.0a15
|
||||
typing-extensions==4.13.2
|
||||
typing-inspection==0.4.0
|
||||
tzdata==2025.2
|
||||
ujson==5.10.0
|
||||
urllib3==2.2.1
|
||||
websockets==11.0.3
|
||||
wrapt==1.16.0
|
||||
yarl==1.17.1
|
||||
zipp==3.20.2
|
||||
wrapt==1.17.2
|
||||
yarl==1.20.0
|
||||
Generated
+758
-700
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -1,7 +1,7 @@
|
||||
[tool.poetry]
|
||||
name = "speckle-qgis"
|
||||
name = "speckle-qgis-v3"
|
||||
version = "0.1.0"
|
||||
description = "poetry for speckle-qgis"
|
||||
description = "poetry for speckle-qgis-v3"
|
||||
authors = ["Your Name <you@example.com>"]
|
||||
license = "Apache License 2.0"
|
||||
readme = "README.md"
|
||||
@@ -17,7 +17,7 @@ geopandas = "0.13.2"
|
||||
geovoronoi = "0.4.0"
|
||||
scipy = "^1.13.0"
|
||||
earcut = "1.1.5"
|
||||
specklepy = {version = "^3.0.0a1", allow-prereleases = true}
|
||||
specklepy = {version = "^3.0.0a15", allow-prereleases = true}
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^7.1.3"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.2.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build", "ci-build\build.csproj", "{7DF908B3-503D-51C2-3675-626DC2895B77}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{7DF908B3-503D-51C2-3675-626DC2895B77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7DF908B3-503D-51C2-3675-626DC2895B77}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7DF908B3-503D-51C2-3675-626DC2895B77}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7DF908B3-503D-51C2-3675-626DC2895B77}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {91042CB4-A71D-4F55-8534-D9A7A1760347}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -35,7 +35,7 @@ class QgisBasicConnectorBinding(IBasicConnectorBinding):
|
||||
self.parent = bridge
|
||||
self.commands = BasicConnectorBindingCommands(bridge)
|
||||
|
||||
self.store.document_changed = lambda: self.commands.notify_document_changed()
|
||||
# self.store.document_changed = lambda: self.commands.notify_document_changed()
|
||||
|
||||
def get_source_app_name(self) -> str:
|
||||
# TODO self.speckle_application.slug
|
||||
@@ -127,13 +127,6 @@ class QgisSendBinding(ISendBinding, QObject, metaclass=MetaQObject):
|
||||
self.commads = SendBindingUICommands(bridge)
|
||||
self.subscribe_to_qgis_events()
|
||||
|
||||
def new_func():
|
||||
self.store.document_changed()
|
||||
# TODO
|
||||
# self.send_conversion_cache.clear_cache()
|
||||
|
||||
self.store.document_changed = new_func
|
||||
|
||||
def subscribe_to_qgis_events(self):
|
||||
# TODO
|
||||
return
|
||||
|
||||
@@ -5,15 +5,38 @@ from speckle.ui.models import DocumentModelStore
|
||||
from specklepy.objects.models.collections.collection import Collection
|
||||
from specklepy.objects.proxies import ColorProxy
|
||||
|
||||
from PyQt5.QtGui import QColor
|
||||
from qgis.core import QgsLayerTreeGroup, QgsVectorLayer, QgsRasterLayer, QgsFeature
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, QTimer, QVariant
|
||||
|
||||
|
||||
class QgisDocumentStore(DocumentModelStore):
|
||||
def __init__(self):
|
||||
class MetaQObject(type(QObject), type(DocumentModelStore)):
|
||||
# avoiding TypeError: metaclass conflict: the metaclass of a derived class
|
||||
# must be a (non-strict) subclass of the metaclasses of all its bases
|
||||
pass
|
||||
|
||||
|
||||
class QgisDocumentStore(DocumentModelStore, QObject, metaclass=MetaQObject):
|
||||
|
||||
document_changed_signal = pyqtSignal()
|
||||
|
||||
def __init__(self, iface):
|
||||
QObject.__init__(self)
|
||||
self.models = []
|
||||
self.is_document_init = False
|
||||
|
||||
# connect to reading document from disk
|
||||
iface.projectRead.connect(
|
||||
lambda: QTimer.singleShot(0, self.on_document_changed)
|
||||
)
|
||||
|
||||
# connect to creating new document
|
||||
iface.newProjectCreated.connect(
|
||||
lambda: QTimer.singleShot(0, self.on_document_changed)
|
||||
)
|
||||
|
||||
def document_changed(self):
|
||||
self.document_changed_signal.emit()
|
||||
|
||||
def on_project_closing(self):
|
||||
return
|
||||
|
||||
@@ -95,7 +118,10 @@ class QgisLayerUnpacker:
|
||||
if isinstance(layer, QgsVectorLayer):
|
||||
layer_fields: Dict[str, Any] = {}
|
||||
for field in layer.fields():
|
||||
layer_fields[field.name()] = field.type()
|
||||
|
||||
# rename reserved property name 'id' (here and in PropertiesExtractor)
|
||||
field_name = field.name() if field.name() != "id" else "ID"
|
||||
layer_fields[field_name] = field.type()
|
||||
|
||||
collection["fields"] = layer_fields
|
||||
collection["wkbType"] = layer.wkbType().name
|
||||
@@ -183,8 +209,11 @@ class QgisColorUnpacker:
|
||||
) -> Any:
|
||||
|
||||
feature_value_for_rendering = feature.attribute(self.stored_renderer_field)
|
||||
category_index = renderer.categoryIndexForValue(feature_value_for_rendering)
|
||||
value_symbol = renderer.categories()[category_index].symbol()
|
||||
if not isinstance(feature_value_for_rendering, QVariant): # for QVariant.NULL
|
||||
category_index = renderer.categoryIndexForValue(feature_value_for_rendering)
|
||||
value_symbol = renderer.categories()[category_index].symbol()
|
||||
else:
|
||||
value_symbol = renderer.sourceSymbol()
|
||||
|
||||
if not value_symbol:
|
||||
value_symbol = renderer.sourceSymbol()
|
||||
@@ -197,7 +226,11 @@ class QgisColorUnpacker:
|
||||
) -> Any:
|
||||
|
||||
feature_value_for_rendering = feature.attribute(self.stored_renderer_field)
|
||||
value_symbol = renderer.symbolForValue(feature_value_for_rendering)
|
||||
if not isinstance(feature_value_for_rendering, QVariant): # for QVariant.NULL
|
||||
value_symbol = renderer.symbolForValue(feature_value_for_rendering)
|
||||
else:
|
||||
value_symbol = renderer.sourceSymbol()
|
||||
|
||||
if not value_symbol:
|
||||
value_symbol = renderer.sourceSymbol()
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from specklepy.objects.geometry import Region
|
||||
from speckle.ui.bindings import SelectionInfo
|
||||
from speckle.ui.models import ModelCard, SenderModelCard
|
||||
|
||||
@@ -234,3 +235,35 @@ class QgisLayerUtils:
|
||||
selected_object_ids=[layer.id or layer.name for layer in selected_layers],
|
||||
summary=f"{len(selected_layers)} layers ({", ".join(object_types)})",
|
||||
)
|
||||
|
||||
|
||||
def confirm_features_type(self, converted_features: List["QgisObject"]) -> None:
|
||||
|
||||
# check if it's a Polygon layer and it has vertical data (needs to be converted to Meshes)
|
||||
convert_regions_to_meshes = False
|
||||
for feature in converted_features:
|
||||
polygon_dataset = True
|
||||
for display_region in feature.displayValue:
|
||||
if not isinstance(display_region, Region):
|
||||
polygon_dataset = False
|
||||
break
|
||||
else: # if Region
|
||||
if getattr(
|
||||
display_region, "3d", None
|
||||
): # sufficient condition to convert all dataset features to meshes
|
||||
convert_regions_to_meshes = True
|
||||
break
|
||||
|
||||
if convert_regions_to_meshes or not polygon_dataset:
|
||||
break
|
||||
|
||||
# modify list of features if needed
|
||||
if convert_regions_to_meshes:
|
||||
for i, feature in enumerate(converted_features):
|
||||
display_meshes = []
|
||||
|
||||
# replace displayValue of Region list to Mesh list
|
||||
for display_region in feature.displayValue: # region
|
||||
display_meshes.extend(display_region.displayValue)
|
||||
|
||||
converted_features[i].displayValue = display_meshes
|
||||
|
||||
@@ -19,12 +19,14 @@ from speckle.ui.models import SendInfo
|
||||
from specklepy.objects.base import Base
|
||||
|
||||
# from specklepy.objects.data import QgisObject
|
||||
from specklepy.objects.geometry.mesh import Mesh
|
||||
from specklepy.objects.geometry import Mesh
|
||||
from specklepy.objects.models.collections.collection import Collection
|
||||
|
||||
from qgis.core import QgsProject, QgsVectorLayer, QgsRasterLayer
|
||||
|
||||
from speckle.host_apps.qgis.connectors.utils import UNSUPPORTED_PROVIDERS
|
||||
from specklepy.objects.other import RenderMaterial
|
||||
from specklepy.objects.proxies import RenderMaterialProxy
|
||||
|
||||
|
||||
class QgisRootObjectBuilder(IRootObjectBuilder):
|
||||
@@ -148,6 +150,17 @@ class QgisRootObjectBuilder(IRootObjectBuilder):
|
||||
self.color_unpacker.color_proxy_cache.values()
|
||||
)
|
||||
|
||||
# duplicate colors into render materials
|
||||
root_collection[ProxyKeys().RENDER_MATERIAL] = [
|
||||
RenderMaterialProxy(
|
||||
objects=x.objects,
|
||||
value=RenderMaterial(
|
||||
applicationId=x.applicationId, name=x.applicationId, diffuse=x.value
|
||||
),
|
||||
)
|
||||
for x in self.color_unpacker.color_proxy_cache.values()
|
||||
]
|
||||
|
||||
return RootObjectBuilderResult(
|
||||
root_object=root_collection,
|
||||
conversion_results=results,
|
||||
@@ -174,6 +187,9 @@ class QgisRootObjectBuilder(IRootObjectBuilder):
|
||||
feature, get_speckle_app_id(feature, layer_app_id)
|
||||
)
|
||||
|
||||
# for 3d polygons: replace Regions with Meshes
|
||||
self.layer_utils.confirm_features_type(converted_features)
|
||||
|
||||
return converted_features
|
||||
|
||||
def convert_raster_feature(
|
||||
|
||||
@@ -50,7 +50,7 @@ class QgisConnectorModule(QObject):
|
||||
self.iface = iface
|
||||
self.bridge = bridge
|
||||
self.thread_context = QgisThreadContext()
|
||||
self.document_store = QgisDocumentStore()
|
||||
self.document_store = QgisDocumentStore(iface)
|
||||
self.basic_binding = QgisBasicConnectorBinding(self.document_store, bridge)
|
||||
self.send_binding = QgisSendBinding(
|
||||
bridge=bridge,
|
||||
|
||||
@@ -1,15 +1,52 @@
|
||||
import os
|
||||
from typing import Callable
|
||||
from pathlib import Path
|
||||
from specklepy.logging import metrics
|
||||
|
||||
from speckle.sdk.connectors_common.threading import ThreadContext
|
||||
|
||||
from qgis.core import Qgis
|
||||
from qgis.core import Qgis, QgsApplication
|
||||
|
||||
UNSUPPORTED_PROVIDERS = ["WFS", "wms", "wcs", "vectortile"]
|
||||
HOST_APP_FULL_VERSION = (
|
||||
Qgis.QGIS_VERSION.encode("iso-8859-1", errors="ignore")
|
||||
.decode("utf-8")
|
||||
.split("-")[0]
|
||||
)
|
||||
UNSUPPORTED_PROVIDERS = ["WFS", "wms", "wcs", "vectortile"]
|
||||
|
||||
|
||||
def get_core_version():
|
||||
|
||||
metadata_path = os.path.join(
|
||||
QgsApplication.qgisSettingsDirPath(),
|
||||
"python",
|
||||
"plugins",
|
||||
"speckle-qgis-v3",
|
||||
"metadata.txt",
|
||||
)
|
||||
core_version = "3.0.099-alpha"
|
||||
with open(metadata_path, "r") as file:
|
||||
for i, line in enumerate(file.readlines()):
|
||||
if "version=" in line:
|
||||
core_version = line.replace("version=", "").replace("\n", "")
|
||||
break
|
||||
file.close()
|
||||
|
||||
return core_version
|
||||
|
||||
|
||||
CORE_VERSION = get_core_version()
|
||||
|
||||
|
||||
def setup_metrics():
|
||||
|
||||
# set hostApp and hostAppVersion
|
||||
version = (
|
||||
Qgis.QGIS_VERSION.encode("iso-8859-1", errors="ignore")
|
||||
.decode("utf-8")
|
||||
.split(".")[0]
|
||||
)
|
||||
metrics.set_host_app("qgis", version)
|
||||
|
||||
|
||||
class QgisThreadContext(ThreadContext):
|
||||
|
||||
@@ -14,9 +14,8 @@ from qgis.core import (
|
||||
QgsRasterLayer,
|
||||
QgsGeometry,
|
||||
QgsCoordinateTransform,
|
||||
QgsPoint,
|
||||
)
|
||||
from osgeo import gdal
|
||||
from PyQt5 import QtCore
|
||||
|
||||
|
||||
class DisplayValueExtractor:
|
||||
@@ -93,7 +92,24 @@ class PropertiesExtractor:
|
||||
def get_properties(self, core_object: Any) -> Dict[str, Any]:
|
||||
|
||||
if isinstance(core_object, QgsFeature):
|
||||
return core_object.attributeMap()
|
||||
# print(core_object.attributeMap()) # shortcut, but we need special treatment for certain data types, therefore using the loop below
|
||||
|
||||
properties = {}
|
||||
for field in core_object.fields():
|
||||
|
||||
# rename reserved property name (here and in create_and_cache_layer_collection)
|
||||
name: str = field.name() if field.name() != "id" else "ID"
|
||||
value = core_object[field.name()]
|
||||
|
||||
# convert values unfamiliar to our Serializer to String or Null
|
||||
if type(value) is QtCore.QVariant:
|
||||
value = None
|
||||
elif type(value) is QtCore.QDate or QtCore.QDateTime or QtCore.QTime:
|
||||
value = str(value)
|
||||
|
||||
properties[name] = value
|
||||
|
||||
return properties
|
||||
|
||||
elif isinstance(core_object, QgsRasterLayer):
|
||||
return {} # TODO
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import math
|
||||
from typing import List
|
||||
|
||||
import earcut.earcut
|
||||
from specklepy.objects.geometry.mesh import Mesh
|
||||
from specklepy.objects.geometry.polyline import Polyline
|
||||
|
||||
|
||||
def generate_region_mesh(boundary: Polyline, inner_loops: List[Polyline], units: str):
|
||||
"""Generate Speckle Mesh for a planar shape represented by boundary and inner loops."""
|
||||
|
||||
# Get a 'list of coordinate tuples' for boundary points
|
||||
vertices3d_tuples: List[List[float]] = _flat_coords_to_tuples(boundary)
|
||||
|
||||
# Get a list of 'lists of coordinate tuples' for inner loops
|
||||
loops3d_tuples_list: List[List[List[float]]] = []
|
||||
for loop in inner_loops:
|
||||
vertices3d_loop_tuples = _flat_coords_to_tuples(loop)
|
||||
loops3d_tuples_list.append(vertices3d_loop_tuples)
|
||||
|
||||
# triangulate region
|
||||
all_coords, triangles = _get_all_coords_and_triangles(
|
||||
vertices3d_tuples, loops3d_tuples_list
|
||||
)
|
||||
|
||||
# construct mesh
|
||||
mesh: Mesh = _construct_mesh_from_triangles(all_coords, triangles, units)
|
||||
|
||||
return mesh
|
||||
|
||||
|
||||
def _flat_coords_to_tuples(polyline: Polyline):
|
||||
"""Reduce resolution of the given polyline (if vertices exceed max amount),
|
||||
and return the list of vertices' coordinate tuples."""
|
||||
|
||||
max_points = 1000
|
||||
coef = math.ceil(len(polyline.value) / (3 * max_points))
|
||||
|
||||
# Get a list of coordinate tuples for polyline points
|
||||
points_count = int(len(polyline.value) / 3)
|
||||
coordinates_tuples: List[List[float]] = [
|
||||
(
|
||||
polyline.value[i * coef * 3],
|
||||
polyline.value[i * coef * 3 + 1],
|
||||
polyline.value[i * coef * 3 + 2],
|
||||
)
|
||||
for i, _ in enumerate(polyline.value)
|
||||
if i * coef < points_count
|
||||
]
|
||||
return coordinates_tuples
|
||||
|
||||
|
||||
def _get_all_coords_and_triangles(
|
||||
vertices3d_tuples: List[List[float]], loops3d_tuples: List[List[List[float]]]
|
||||
):
|
||||
"""Triangulate the shape given tuple lists of boundary and loops' coordinates.
|
||||
Return full flat list of triangulated vertices and list of triangle tuples."""
|
||||
|
||||
data = earcut.earcut.flatten([vertices3d_tuples] + loops3d_tuples)
|
||||
triangles_flat_list = earcut.earcut.earcut(data["vertices"], data["holes"], dim=3)
|
||||
|
||||
triangle_tuples = [
|
||||
[
|
||||
triangles_flat_list[3 * i],
|
||||
triangles_flat_list[3 * i + 1],
|
||||
triangles_flat_list[3 * i + 2],
|
||||
]
|
||||
for i, _ in enumerate(triangles_flat_list)
|
||||
if i < len(triangles_flat_list) / 3
|
||||
]
|
||||
|
||||
return data["vertices"], triangle_tuples
|
||||
|
||||
|
||||
def _construct_mesh_from_triangles(all_coords, triangles, units) -> Mesh:
|
||||
"""Construct Speckle Mesh given a flat list of coordinates and a list of triangles
|
||||
(defined by tuples with vertices' indices)."""
|
||||
|
||||
total_vertices = 0
|
||||
vertices = []
|
||||
faces = []
|
||||
|
||||
for trg in triangles:
|
||||
|
||||
# make sure all faces are clockwise (facing down). Seems earcut already returns clockwise faces
|
||||
vertices.extend(
|
||||
all_coords[3 * trg[0] : 3 * trg[0] + 3]
|
||||
+ all_coords[3 * trg[1] : 3 * trg[1] + 3]
|
||||
+ all_coords[3 * trg[2] : 3 * trg[2] + 3]
|
||||
)
|
||||
|
||||
faces.extend(
|
||||
[
|
||||
3,
|
||||
total_vertices,
|
||||
total_vertices + 1,
|
||||
total_vertices + 2,
|
||||
]
|
||||
)
|
||||
total_vertices += 3
|
||||
|
||||
return Mesh(vertices=vertices, faces=faces, units=units)
|
||||
@@ -3,9 +3,9 @@ from typing import List
|
||||
from speckle.host_apps.qgis.connectors.extensions import get_speckle_app_id
|
||||
from speckle.host_apps.qgis.converters.settings import QgisConversionSettings
|
||||
|
||||
from specklepy.objects.geometry.point import Point
|
||||
from specklepy.objects.geometry.polyline import Polyline
|
||||
from specklepy.objects.geometry.mesh import Mesh
|
||||
from speckle.host_apps.qgis.converters.to_speckle.mesher import generate_region_mesh
|
||||
from specklepy.objects.base import Base
|
||||
from specklepy.objects.geometry import Mesh, Point, Polyline, Region
|
||||
|
||||
from qgis.core import (
|
||||
QgsAbstractGeometry,
|
||||
@@ -124,7 +124,7 @@ class PolygonToSpeckleConverter:
|
||||
self._conversion_settings = conversion_settings
|
||||
self._polyline_converter = polyline_converter
|
||||
|
||||
def convert(self, target: QgsAbstractGeometry) -> List[Polyline]:
|
||||
def convert(self, target: QgsAbstractGeometry) -> List[Base]:
|
||||
|
||||
wkb_type = target.wkbType()
|
||||
|
||||
@@ -142,18 +142,39 @@ class PolygonToSpeckleConverter:
|
||||
or wkb_type == QgsWkbTypes.CurvePolygonM
|
||||
or wkb_type == QgsWkbTypes.CurvePolygonZM
|
||||
):
|
||||
all_curves = []
|
||||
all_regions = []
|
||||
all_z_values = []
|
||||
for part in target.parts():
|
||||
|
||||
all_curves.append(
|
||||
self._polyline_converter.convert(part.exteriorRing())[0]
|
||||
boundary = self._polyline_converter.convert(part.exteriorRing())[0]
|
||||
all_z_values.extend(
|
||||
[x for i, x in enumerate(boundary.value) if (i + 1) % 3 == 0]
|
||||
)
|
||||
inner_loops = []
|
||||
|
||||
for i in range(part.numInteriorRings()):
|
||||
all_curves.append(
|
||||
inner_loops.append(
|
||||
self._polyline_converter.convert(part.interiorRing(i))[0]
|
||||
)
|
||||
return all_curves
|
||||
|
||||
display_mesh: Mesh = generate_region_mesh(
|
||||
boundary, inner_loops, self._conversion_settings.speckle_units
|
||||
)
|
||||
new_region = Region(
|
||||
boundary=boundary,
|
||||
innerLoops=inner_loops,
|
||||
hasHatchPattern=False,
|
||||
displayValue=[display_mesh],
|
||||
units=self._conversion_settings.speckle_units,
|
||||
)
|
||||
# hacky way to indicate that all features in the dataset should be sent as Meshes instead of Regions
|
||||
if len(list(set(all_z_values))) > 1:
|
||||
new_region["3d"] = True
|
||||
|
||||
all_regions.append(new_region)
|
||||
|
||||
# return list of Meshes, if not horizontal Polygon
|
||||
return all_regions
|
||||
|
||||
raise ValueError(f"Geometry of type '{type(target)}' cannot be converted")
|
||||
|
||||
|
||||
@@ -31,9 +31,9 @@ class SpeckleQGISv3Module:
|
||||
self.dockwidget = SpeckleQGISv3Dialog(
|
||||
bridge=self, basic_binding=self.connector_module.basic_binding
|
||||
)
|
||||
self.dockwidget.runSetup(self)
|
||||
self.dockwidget.header_widget = self.dockwidget.create_header(self)
|
||||
self.dockwidget.runSetup()
|
||||
self.connect_dockwidget_signals()
|
||||
self.connect_self_signals()
|
||||
|
||||
def instantiate_module_dependencies(self, iface):
|
||||
|
||||
@@ -59,16 +59,20 @@ class SpeckleQGISv3Module:
|
||||
self.dockwidget.add_send_notification
|
||||
) # Send a UI notification after Send operation
|
||||
|
||||
# all dockwidget subscribtions to child widget signals are handled in Dockwidget class,
|
||||
# because child widget are not persistent
|
||||
# refresh widgets if document change signal received
|
||||
self.connector_module.document_store.document_changed_signal.connect(
|
||||
self.dockwidget.refresh_ui
|
||||
)
|
||||
|
||||
def connect_self_signals(self):
|
||||
# signal to update UI, needs t be transferred to the main thread
|
||||
# signal to update UI, needs to be transferred to the main thread
|
||||
self.dockwidget.activity_start_signal.connect(
|
||||
self.dockwidget.add_activity_status
|
||||
)
|
||||
# all dockwidget subscribtions to child widget signals are handled in Dockwidget class,
|
||||
# because child widget are not persistent
|
||||
|
||||
def connect_connector_module_signals(self):
|
||||
# create conversion settings and RootObjectBuilder
|
||||
self.connector_module.send_binding.create_send_modules_signal.connect(
|
||||
self._create_send_modules
|
||||
)
|
||||
@@ -92,24 +96,29 @@ class SpeckleQGISv3Module:
|
||||
on_operation_progressed: "IProgress[CardProgress]",
|
||||
ct: "CancellationToken",
|
||||
):
|
||||
|
||||
# first, update UI status
|
||||
self.dockwidget.activity_start_signal.emit(
|
||||
model_card_id, "Converting and sending.."
|
||||
)
|
||||
|
||||
print("_execute_send_operation, send_operation.execute:")
|
||||
# execute and return send operation results
|
||||
send_operation_result: SendOperationResult = (
|
||||
self.connector_module.send_operation.execute(
|
||||
objects, send_info, on_operation_progressed, ct
|
||||
# wrap into exception handler, which will cancel task and UI progress, instead of giving ipression that task still loads
|
||||
try:
|
||||
# first, update UI status
|
||||
self.dockwidget.activity_start_signal.emit(
|
||||
model_card_id, "Converting and sending.."
|
||||
)
|
||||
)
|
||||
self.connector_module.send_binding.commads.set_model_send_result(
|
||||
model_card_id=model_card_id,
|
||||
version_id=send_operation_result.root_obj_id,
|
||||
send_conversion_results=send_operation_result.converted_references,
|
||||
)
|
||||
|
||||
print("_execute_send_operation -> send_operation.execute:")
|
||||
# execute and return send operation results
|
||||
send_operation_result: SendOperationResult = (
|
||||
self.connector_module.send_operation.execute(
|
||||
objects, send_info, on_operation_progressed, ct
|
||||
)
|
||||
)
|
||||
self.connector_module.send_binding.commads.set_model_send_result(
|
||||
model_card_id=model_card_id,
|
||||
version_id=send_operation_result.root_obj_id,
|
||||
send_conversion_results=send_operation_result.converted_references,
|
||||
)
|
||||
except Exception as e:
|
||||
# TODO: also show an error message
|
||||
print(e)
|
||||
self._cancel_operation(model_card_id)
|
||||
|
||||
def _cancel_operation(self, model_card_id: str):
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from speckle.host_apps.qgis.connectors.utils import HOST_APP_FULL_VERSION
|
||||
from speckle.host_apps.qgis.connectors.utils import HOST_APP_FULL_VERSION, CORE_VERSION
|
||||
from speckle.sdk.connectors_common.api import IClientFactory, IOperations
|
||||
from speckle.sdk.connectors_common.builders import (
|
||||
IRootObjectBuilder,
|
||||
@@ -136,7 +136,7 @@ class SendOperation:
|
||||
account,
|
||||
{
|
||||
"hostAppFullVersion": HOST_APP_FULL_VERSION,
|
||||
"core_version": "3.0.099",
|
||||
"core_version": CORE_VERSION,
|
||||
"ui": "dui3",
|
||||
"workspace_id": get_project_workspace_id(client, send_info.project_id),
|
||||
},
|
||||
@@ -153,11 +153,11 @@ class SendOperation:
|
||||
|
||||
_ = api_client.version.create(
|
||||
CreateVersionInput(
|
||||
objectId=obj_id,
|
||||
modelId=send_info.model_id,
|
||||
projectId=send_info.project_id,
|
||||
object_id=obj_id,
|
||||
model_id=send_info.model_id,
|
||||
project_id=send_info.project_id,
|
||||
message="Sent from QGIS v3",
|
||||
sourceApplication=send_info.host_application,
|
||||
source_application=send_info.host_application,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -16,5 +16,5 @@ def get_project_workspace_id(client: SpeckleClient, project_id: str) -> Optional
|
||||
min = server_version[1]
|
||||
|
||||
if maj > 2 or (maj == 2 and min > 20):
|
||||
workspace_id = client.project.get(project_id).workspaceId
|
||||
workspace_id = client.project.get(project_id).workspace_id
|
||||
return workspace_id
|
||||
|
||||
@@ -9,6 +9,7 @@ from specklepy.core.api.models.current import (
|
||||
ResourceCollection,
|
||||
)
|
||||
from specklepy.core.api.resources.current.project_resource import ProjectResource
|
||||
from specklepy.core.api.resources.current.workspace_resource import Workspace
|
||||
from speckle.ui.utils.utils import (
|
||||
create_new_project_query,
|
||||
create_new_model_query,
|
||||
@@ -28,6 +29,7 @@ class UiSearchUtils(QObject):
|
||||
cursor_projects: Any = None
|
||||
cursor_models: Any = None
|
||||
speckle_client: SpeckleClient = None
|
||||
current_workspace: Optional[Workspace] = None
|
||||
batch_size: int = None
|
||||
add_selection_filter_signal = pyqtSignal(SenderModelCard)
|
||||
add_models_search_signal = pyqtSignal(Project)
|
||||
@@ -36,30 +38,25 @@ class UiSearchUtils(QObject):
|
||||
new_model_widget_signal = pyqtSignal(str)
|
||||
change_account_and_projects_signal = pyqtSignal()
|
||||
refresh_models_signal = pyqtSignal()
|
||||
open_add_new_account_widget_signal = pyqtSignal()
|
||||
add_new_account_signal = pyqtSignal()
|
||||
|
||||
clear_project_search_bar_signal = pyqtSignal()
|
||||
clear_model_search_bar_signal = pyqtSignal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
accounts: List[Account] = get_accounts()
|
||||
if len(accounts) == 0: # TODO handle no local accounts
|
||||
raise SpeckleException(
|
||||
"Add accounts via Speckle Desktop Manager in order to start"
|
||||
)
|
||||
|
||||
self.speckle_client: SpeckleClient = get_authenticate_client_for_account(
|
||||
accounts[0]
|
||||
)
|
||||
self.batch_size = QUERY_BATCH_SIZE
|
||||
|
||||
def get_accounts_content(self):
|
||||
accounts: List[Account] = get_accounts()
|
||||
if len(accounts) == 0: # TODO handle no local accounts
|
||||
raise SpeckleException(
|
||||
"Add accounts via Speckle Desktop Manager in order to start"
|
||||
if len(accounts) > 0:
|
||||
self.speckle_client: SpeckleClient = get_authenticate_client_for_account(
|
||||
accounts[0]
|
||||
)
|
||||
|
||||
def get_accounts_content(self) -> List[List[Any]]:
|
||||
accounts: List[Account] = get_accounts()
|
||||
|
||||
content_list = [
|
||||
[
|
||||
partial(self._replace_projects_list_with_new_account, acc),
|
||||
@@ -77,11 +74,13 @@ class UiSearchUtils(QObject):
|
||||
self.change_account_and_projects_signal.emit()
|
||||
|
||||
def get_account_initials(self):
|
||||
if self.speckle_client is None:
|
||||
return "?"
|
||||
name = self.speckle_client.account.userInfo.name
|
||||
if isinstance(name, str) and len(name) > 0:
|
||||
return name[0]
|
||||
|
||||
return "X"
|
||||
return "?"
|
||||
|
||||
def create_new_project(self, name: str, workspace_id: Optional[str] = None):
|
||||
create_new_project_query(self.speckle_client, name, workspace_id)
|
||||
@@ -90,6 +89,9 @@ class UiSearchUtils(QObject):
|
||||
create_new_model_query(self.speckle_client, project_id, model_name)
|
||||
|
||||
def get_new_projects_content(self, clear_cursor=False):
|
||||
workspace_id: Optional[str] = (
|
||||
self.current_workspace.id if self.current_workspace else None
|
||||
)
|
||||
|
||||
if clear_cursor:
|
||||
self.cursor_projects = None
|
||||
@@ -97,9 +99,18 @@ class UiSearchUtils(QObject):
|
||||
content_list: List[List] = []
|
||||
projects_resource_collection: ResourceCollection[Project] = (
|
||||
get_projects_from_client(
|
||||
speckle_client=self.speckle_client, cursor=self.cursor_projects
|
||||
speckle_client=self.speckle_client,
|
||||
workspace_id=workspace_id,
|
||||
cursor=self.cursor_projects,
|
||||
)
|
||||
)
|
||||
|
||||
# filter out projects without workspace (if None)
|
||||
if self.current_workspace is None:
|
||||
projects_resource_collection.items = [
|
||||
x for x in projects_resource_collection.items if x.workspace_id is None
|
||||
]
|
||||
|
||||
self.cursor_projects = projects_resource_collection.cursor
|
||||
content_list: List[List] = (
|
||||
self._create_project_content_list_from_resource_collection(
|
||||
@@ -111,15 +122,27 @@ class UiSearchUtils(QObject):
|
||||
|
||||
def get_new_projects_content_with_name_condition(self, name_filter: str):
|
||||
|
||||
workspace_id: Optional[str] = (
|
||||
self.current_workspace.id if self.current_workspace else None
|
||||
)
|
||||
|
||||
self.cursor_projects = None
|
||||
|
||||
projects_resource_collection: ResourceCollection[Project] = (
|
||||
get_projects_from_client(
|
||||
speckle_client=self.speckle_client,
|
||||
workspace_id=workspace_id,
|
||||
cursor=self.cursor_projects,
|
||||
filter_keyword=name_filter,
|
||||
)
|
||||
)
|
||||
|
||||
# filter out projects without workspace (if None)
|
||||
if self.current_workspace is None:
|
||||
projects_resource_collection.items = [
|
||||
x for x in projects_resource_collection.items if x.workspace_id is None
|
||||
]
|
||||
|
||||
self.cursor_projects = projects_resource_collection.cursor
|
||||
content_list: List[List] = (
|
||||
self._create_project_content_list_from_resource_collection(
|
||||
@@ -137,12 +160,15 @@ class UiSearchUtils(QObject):
|
||||
content_list: List[List] = []
|
||||
|
||||
for project in projects_batch:
|
||||
|
||||
role = "can edit" if project.role is None else project.role.split(":")[-1]
|
||||
|
||||
# make sure to pass the actual project, not a reference to a variable
|
||||
project_content = [
|
||||
partial(self._emit_function_add_models_signal, project),
|
||||
project.name,
|
||||
project.role.split(":")[-1],
|
||||
f"updated {time_ago(project.updatedAt)}",
|
||||
role,
|
||||
f"updated {time_ago(project.updated_at)}",
|
||||
]
|
||||
content_list.append(project_content)
|
||||
return content_list
|
||||
@@ -205,7 +231,7 @@ class UiSearchUtils(QObject):
|
||||
model_content = [
|
||||
partial(self.add_selection_filter_widget, project, model),
|
||||
model.name,
|
||||
f"updated {time_ago(model.updatedAt)}",
|
||||
f"updated {time_ago(model.updated_at)}",
|
||||
project,
|
||||
]
|
||||
content_list.append(model_content)
|
||||
@@ -231,6 +257,12 @@ class UiSearchUtils(QObject):
|
||||
)
|
||||
)
|
||||
|
||||
def get_workspaces(self) -> List[Workspace]:
|
||||
if self.speckle_client is None:
|
||||
return []
|
||||
workspaces = self.speckle_client.active_user.get_workspaces().items
|
||||
return workspaces
|
||||
|
||||
def get_version_search_widget_content(self, project: ProjectResource) -> List[List]:
|
||||
"""Add search cards for models (only valid for Receive workflow)."""
|
||||
|
||||
|
||||
+77
-16
@@ -9,8 +9,10 @@ from specklepy.core.api.inputs.model_inputs import CreateModelInput
|
||||
from specklepy.core.api.inputs.project_inputs import (
|
||||
ProjectCreateInput,
|
||||
ProjectModelsFilter,
|
||||
WorkspaceProjectCreateInput,
|
||||
)
|
||||
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter
|
||||
from specklepy.core.api.inputs.project_inputs import WorksaceProjectsFilter
|
||||
from specklepy.core.api.models.current import (
|
||||
Model,
|
||||
Project,
|
||||
@@ -49,19 +51,51 @@ def get_authenticate_client_for_account(account: Account) -> SpeckleClient:
|
||||
|
||||
|
||||
def get_projects_from_client(
|
||||
speckle_client: SpeckleClient, cursor=None, filter_keyword: Optional[str] = None
|
||||
speckle_client: SpeckleClient,
|
||||
workspace_id: Optional[str],
|
||||
cursor=None,
|
||||
filter_keyword: Optional[str] = None,
|
||||
) -> ResourceCollection[Project]:
|
||||
|
||||
results = []
|
||||
|
||||
# create search filters for user query and workspace query
|
||||
project_user_filter = UserProjectsFilter(search="", workspaceId=workspace_id)
|
||||
project_workspace_filter = WorksaceProjectsFilter(
|
||||
search="", with_project_role_only=False
|
||||
)
|
||||
|
||||
if speckle_client is not None:
|
||||
# possible GraphQLException
|
||||
results: ResourceCollection[Project] = speckle_client.active_user.get_projects(
|
||||
limit=100 if filter_keyword else QUERY_BATCH_SIZE,
|
||||
cursor=cursor,
|
||||
filter=(
|
||||
UserProjectsFilter(search=filter_keyword) if filter_keyword else None
|
||||
),
|
||||
)
|
||||
|
||||
# for personal projects, use active_user query
|
||||
if workspace_id is None:
|
||||
if isinstance(filter_keyword, str):
|
||||
project_user_filter.search = filter_keyword
|
||||
|
||||
# possible GraphQLException
|
||||
results: ResourceCollection[Project] = (
|
||||
speckle_client.active_user.get_projects(
|
||||
limit=100 if filter_keyword else QUERY_BATCH_SIZE,
|
||||
cursor=cursor,
|
||||
filter=project_user_filter,
|
||||
)
|
||||
)
|
||||
|
||||
# for workspace projects, use workspace query (active user.get_projects doesn't return projects created by others, even for admin role)
|
||||
else:
|
||||
|
||||
if isinstance(filter_keyword, str):
|
||||
project_workspace_filter.search = filter_keyword
|
||||
|
||||
# possible GraphQLException
|
||||
results: ResourceCollection[Project] = (
|
||||
speckle_client.workspace.get_projects(
|
||||
workspace_id=workspace_id,
|
||||
limit=100 if filter_keyword else QUERY_BATCH_SIZE,
|
||||
cursor=cursor,
|
||||
filter=project_workspace_filter,
|
||||
)
|
||||
)
|
||||
|
||||
if not isinstance(results, ResourceCollection):
|
||||
# TODO: handle
|
||||
@@ -71,6 +105,20 @@ def get_projects_from_client(
|
||||
# TODO add a warning
|
||||
pass
|
||||
|
||||
results.items = [
|
||||
item
|
||||
for item in results.items
|
||||
if (
|
||||
(
|
||||
item.role is None
|
||||
and speckle_client.project.get_permissions(
|
||||
item.id
|
||||
).can_create_model.authorized
|
||||
)
|
||||
or (isinstance(item.role, str) and not item.role.endswith("viewer"))
|
||||
) # "None" for "implicit" owner or viewer roles (if not explicitly invited)
|
||||
]
|
||||
|
||||
return results
|
||||
|
||||
|
||||
@@ -79,7 +127,7 @@ def get_models_from_client(
|
||||
project: Project,
|
||||
cursor=None,
|
||||
filter_keyword: Optional[str] = None,
|
||||
) -> ResourceCollection[Project]:
|
||||
) -> ResourceCollection[Model]:
|
||||
|
||||
results = []
|
||||
if speckle_client is not None:
|
||||
@@ -130,12 +178,25 @@ def create_new_project_query(
|
||||
|
||||
result = None
|
||||
if speckle_client is not None:
|
||||
# possible GraphQLException
|
||||
result: Project = speckle_client.project.create(
|
||||
input=ProjectCreateInput(
|
||||
name=project_name, description=None, visibility=None
|
||||
|
||||
if workspace_id:
|
||||
# possible GraphQLException
|
||||
result: Project = speckle_client.project.create_in_workspace(
|
||||
input=WorkspaceProjectCreateInput(
|
||||
name=project_name,
|
||||
description=None,
|
||||
visibility=None,
|
||||
workspaceId=workspace_id,
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
# possible GraphQLException
|
||||
result: Project = speckle_client.project.create(
|
||||
input=ProjectCreateInput(
|
||||
name=project_name, description=None, visibility=None
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if not isinstance(result, Project):
|
||||
# TODO: handle
|
||||
@@ -157,7 +218,7 @@ def create_new_model_query(
|
||||
# possible GraphQLException
|
||||
result: Project = speckle_client.model.create(
|
||||
input=CreateModelInput(
|
||||
name=model_name, description=None, projectId=project_id
|
||||
name=model_name, description=None, project_id=project_id
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from speckle.sdk.connectors_common.operations import SendOperationResult
|
||||
from speckle.ui.bindings import IBasicConnectorBinding, SelectionInfo
|
||||
from speckle.ui.models import ModelCard, SenderModelCard
|
||||
from speckle.ui.widgets.widget_account_search import AccountSearchWidget
|
||||
from speckle.ui.widgets.widget_add_account import AddAccountWidget
|
||||
from speckle.ui.widgets.widget_model_card import ModelCardWidget
|
||||
from speckle.ui.widgets.widget_model_cards_list import ModelCardsWidget
|
||||
from speckle.ui.widgets.widget_model_search import ModelSearchWidget
|
||||
@@ -55,6 +56,7 @@ class SpeckleQGISv3Dialog(QDockWidget):
|
||||
widget_project_search: ProjectSearchWidget = None
|
||||
widget_model_search: ModelSearchWidget = None
|
||||
widget_account_search: AccountSearchWidget = None
|
||||
widget_account_add: AddAccountWidget = None
|
||||
widget_new_project: NewProjectWidget = None
|
||||
widget_new_model: NewModelWidget = None
|
||||
widget_model_cards: ModelCardsWidget = None
|
||||
@@ -80,7 +82,7 @@ class SpeckleQGISv3Dialog(QDockWidget):
|
||||
self.basic_binding = basic_binding
|
||||
self.bridge = bridge
|
||||
|
||||
def runSetup(self, plugin):
|
||||
def runSetup(self):
|
||||
self.placeholder_widget = QWidget()
|
||||
self.placeholder_widget.layout = QVBoxLayout(self.placeholder_widget)
|
||||
self.placeholder_widget.layout.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -88,11 +90,10 @@ class SpeckleQGISv3Dialog(QDockWidget):
|
||||
self.placeholder_widget.setStyleSheet(f"{ZERO_MARGIN_PADDING}")
|
||||
self.layout().addWidget(self.placeholder_widget)
|
||||
|
||||
# create and add header widget
|
||||
self.header_widget = self._create_header(plugin)
|
||||
# add header widget
|
||||
self.placeholder_widget.layout.addWidget(self.header_widget)
|
||||
|
||||
# cerate and add main widget
|
||||
# create and add main widget
|
||||
self.main_widget = QWidget()
|
||||
self.main_widget.layout = QStackedLayout(self.main_widget)
|
||||
self.main_widget.layout.setStackingMode(QStackedLayout.StackAll)
|
||||
@@ -101,9 +102,13 @@ class SpeckleQGISv3Dialog(QDockWidget):
|
||||
self.placeholder_widget.layout.addWidget(self.main_widget)
|
||||
|
||||
# add first widget to main
|
||||
self._add_start_widget(plugin)
|
||||
self._add_start_widget()
|
||||
|
||||
def _create_header(self, plugin):
|
||||
def refresh_ui(self):
|
||||
self._remove_all_widgets()
|
||||
self._add_start_widget()
|
||||
|
||||
def create_header(self, plugin):
|
||||
try:
|
||||
header_widget = QWidget()
|
||||
header_widget.setStyleSheet(f"{BACKGR_COLOR}{ZERO_MARGIN_PADDING}")
|
||||
@@ -180,7 +185,7 @@ class SpeckleQGISv3Dialog(QDockWidget):
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
def _add_start_widget(self, plugin):
|
||||
def _add_start_widget(self):
|
||||
|
||||
# document in QGIS is opened by default, we don't need as actually saved file to start working with data
|
||||
document_open = True
|
||||
@@ -218,6 +223,9 @@ class SpeckleQGISv3Dialog(QDockWidget):
|
||||
if self.widget_account_search:
|
||||
self._remove_widget_account_search()
|
||||
|
||||
if self.widget_account_add:
|
||||
self._remove_widget_account_add()
|
||||
|
||||
if self.widget_new_project:
|
||||
self._remove_widget_new_project()
|
||||
|
||||
@@ -225,7 +233,7 @@ class SpeckleQGISv3Dialog(QDockWidget):
|
||||
self._remove_widget_new_model()
|
||||
|
||||
if self.widget_selection_filter:
|
||||
self.remove_widget_selection_filter()
|
||||
self._remove_widget_selection_filter()
|
||||
|
||||
if self.widget_model_cards:
|
||||
self._remove_widget_model_cards()
|
||||
@@ -244,6 +252,9 @@ class SpeckleQGISv3Dialog(QDockWidget):
|
||||
elif self.widget_account_search == widget:
|
||||
self._remove_widget_account_search()
|
||||
|
||||
elif self.widget_account_add == widget:
|
||||
self._remove_widget_account_add()
|
||||
|
||||
elif self.widget_new_project == widget:
|
||||
self._remove_widget_new_project()
|
||||
|
||||
@@ -251,7 +262,7 @@ class SpeckleQGISv3Dialog(QDockWidget):
|
||||
self._remove_widget_new_model()
|
||||
|
||||
elif self.widget_selection_filter == widget:
|
||||
self.remove_widget_selection_filter()
|
||||
self._remove_widget_selection_filter()
|
||||
|
||||
def _remove_process_widgets(self):
|
||||
if self.widget_project_search:
|
||||
@@ -261,11 +272,14 @@ class SpeckleQGISv3Dialog(QDockWidget):
|
||||
self._remove_widget_model_search()
|
||||
|
||||
if self.widget_selection_filter:
|
||||
self.remove_widget_selection_filter()
|
||||
self._remove_widget_selection_filter()
|
||||
|
||||
if self.widget_account_search:
|
||||
self._remove_widget_account_search()
|
||||
|
||||
if self.widget_account_add:
|
||||
self._remove_widget_account_add()
|
||||
|
||||
if self.widget_new_project:
|
||||
self._remove_widget_new_project()
|
||||
|
||||
@@ -284,6 +298,10 @@ class SpeckleQGISv3Dialog(QDockWidget):
|
||||
self.widget_account_search.setParent(None)
|
||||
self.widget_account_search = None
|
||||
|
||||
def _remove_widget_account_add(self):
|
||||
self.widget_account_add.setParent(None)
|
||||
self.widget_account_add = None
|
||||
|
||||
def _remove_widget_new_project(self):
|
||||
self.widget_new_project.setParent(None)
|
||||
self.widget_new_project = None
|
||||
@@ -292,7 +310,7 @@ class SpeckleQGISv3Dialog(QDockWidget):
|
||||
self.widget_new_model.setParent(None)
|
||||
self.widget_new_model = None
|
||||
|
||||
def remove_widget_selection_filter(self):
|
||||
def _remove_widget_selection_filter(self):
|
||||
self.widget_selection_filter.setParent(None)
|
||||
self.widget_selection_filter = None
|
||||
|
||||
@@ -375,6 +393,10 @@ class SpeckleQGISv3Dialog(QDockWidget):
|
||||
self.main_widget.layout.addWidget(self.widget_project_search)
|
||||
self.main_widget.layout.setCurrentWidget(self.widget_project_search)
|
||||
|
||||
# if no accounts are present, open Select Account widget
|
||||
if self.widget_project_search.ui_search_content.speckle_client is None:
|
||||
self._open_select_accounts_widget()
|
||||
|
||||
self.widget_project_search.ui_search_content.add_selection_filter_signal.connect(
|
||||
self._create_selection_filter_widget
|
||||
)
|
||||
@@ -402,6 +424,16 @@ class SpeckleQGISv3Dialog(QDockWidget):
|
||||
self._update_project_list
|
||||
)
|
||||
|
||||
def _update_account_list(self):
|
||||
|
||||
# close AddAccount widget
|
||||
# can be called from AddAccount widget
|
||||
if self.widget_account_add:
|
||||
self._remove_widget_account_add()
|
||||
|
||||
# refresh accounts in the AccountSearch widget
|
||||
self.widget_account_search.refresh_accounts()
|
||||
|
||||
def _update_project_list(self):
|
||||
|
||||
# can be called from CreateAccount or NewProject widgets
|
||||
@@ -410,6 +442,13 @@ class SpeckleQGISv3Dialog(QDockWidget):
|
||||
if self.widget_new_project:
|
||||
self._remove_widget_new_project()
|
||||
|
||||
# get list of workspaces
|
||||
self.widget_project_search.workspaces = (
|
||||
self.widget_project_search.ui_search_content.get_workspaces()
|
||||
)
|
||||
self.widget_project_search._fill_workspace_dropdown()
|
||||
|
||||
# refresh projects for the selected workspace
|
||||
self.widget_project_search.refresh_projects()
|
||||
|
||||
def _update_model_list(self):
|
||||
@@ -474,6 +513,29 @@ class SpeckleQGISv3Dialog(QDockWidget):
|
||||
# subscribe to close-on-background-click event
|
||||
self._subscribe_to_close_on_background_click(self.widget_account_search)
|
||||
|
||||
# subscribe to select_account_signal signal
|
||||
self.widget_account_search.ui_search_content.open_add_new_account_widget_signal.connect(
|
||||
self._open_add_account_widget
|
||||
)
|
||||
|
||||
# subscribe to add_new_account_signal signal
|
||||
self.widget_account_search.ui_search_content.add_new_account_signal.connect(
|
||||
self._update_account_list
|
||||
)
|
||||
|
||||
def _open_add_account_widget(self):
|
||||
if not self.widget_account_add:
|
||||
self.widget_account_add = AddAccountWidget(
|
||||
parent=self,
|
||||
ui_search_content=self.widget_project_search.ui_search_content,
|
||||
)
|
||||
# add widgets to the layout
|
||||
self.main_widget.layout.addWidget(self.widget_account_add)
|
||||
self.main_widget.layout.setCurrentWidget(self.widget_account_add)
|
||||
|
||||
# subscribe to close-on-background-click event
|
||||
self._subscribe_to_close_on_background_click(self.widget_account_add)
|
||||
|
||||
def _open_select_models_widget(self, project):
|
||||
|
||||
if not self.widget_model_search:
|
||||
|
||||
@@ -3,6 +3,13 @@ from speckle.ui.widgets.widget_cards_list_temporary import (
|
||||
CardsListTemporaryWidget,
|
||||
)
|
||||
|
||||
from speckle.ui.widgets.utils.global_resources import (
|
||||
BACKGR_COLOR,
|
||||
BACKGR_COLOR_LIGHT,
|
||||
)
|
||||
|
||||
from PyQt5.QtWidgets import QPushButton, QSizePolicy
|
||||
|
||||
|
||||
class AccountSearchWidget(CardsListTemporaryWidget):
|
||||
|
||||
@@ -13,25 +20,47 @@ class AccountSearchWidget(CardsListTemporaryWidget):
|
||||
*,
|
||||
parent=None,
|
||||
label_text: str = "Select account",
|
||||
ui_search_content: UiSearchUtils = None
|
||||
ui_search_content: UiSearchUtils = None,
|
||||
):
|
||||
self.parent = parent
|
||||
self.ui_search_content = ui_search_content
|
||||
|
||||
# customize load_more function
|
||||
self._load_more = lambda: self._add_accounts(clear_cursor=False)
|
||||
|
||||
# initialize the inherited widget, passing the card content
|
||||
super(AccountSearchWidget, self).__init__(
|
||||
parent=parent, label_text=label_text, cards_content_list=[]
|
||||
parent=parent,
|
||||
label_text=label_text,
|
||||
cards_content_list=[],
|
||||
init_load_more_btn=False,
|
||||
)
|
||||
self.refresh_accounts()
|
||||
|
||||
self._add_accounts(clear_cursor=True)
|
||||
button_create = self._create_add_button()
|
||||
self.scroll_container.layout().addWidget(button_create)
|
||||
|
||||
def _add_accounts(self, clear_cursor=False):
|
||||
def _create_add_button(self) -> QPushButton:
|
||||
|
||||
button_create = QPushButton("Add new account")
|
||||
button_create.clicked.connect(
|
||||
self.ui_search_content.open_add_new_account_widget_signal.emit
|
||||
)
|
||||
button_create.setStyleSheet(
|
||||
"QPushButton {"
|
||||
+ f"color:white;border-radius: 7px;margin:5px;padding: 5px;height: 20px;text-align: center;{BACKGR_COLOR}"
|
||||
+ "} QPushButton:hover { "
|
||||
+ f"{BACKGR_COLOR_LIGHT};"
|
||||
+ " }"
|
||||
)
|
||||
return button_create
|
||||
|
||||
def refresh_accounts(self, clear_cursor=False):
|
||||
|
||||
all_accounts = self.ui_search_content.get_accounts_content()
|
||||
if len(all_accounts) == 0:
|
||||
self.scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||
else:
|
||||
self.scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
|
||||
self._remove_all_cards()
|
||||
self._add_more_cards(
|
||||
all_accounts, clear_cursor, self.ui_search_content.batch_size
|
||||
)
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
from speckle.ui.utils.search_widget_utils import UiSearchUtils
|
||||
from speckle.ui.widgets.background_widget import BackgroundWidget
|
||||
from speckle.ui.widgets.utils.global_resources import (
|
||||
BACKGR_COLOR,
|
||||
BACKGR_COLOR_LIGHT,
|
||||
BACKGR_COLOR_WHITE,
|
||||
WIDGET_SIDE_BUFFER,
|
||||
ZERO_MARGIN_PADDING,
|
||||
)
|
||||
|
||||
import webbrowser
|
||||
|
||||
from PyQt5.QtGui import QColor
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
QStackedLayout,
|
||||
QPushButton,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QGraphicsDropShadowEffect,
|
||||
)
|
||||
|
||||
|
||||
class AddAccountWidget(QWidget):
|
||||
|
||||
ui_search_content: UiSearchUtils = None
|
||||
_message_card: QWidget = (
|
||||
None # needs to be here, so it can be called on resize event
|
||||
)
|
||||
server_url_widget: QLineEdit = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
parent=None,
|
||||
label_text: str = "Add new account",
|
||||
ui_search_content: UiSearchUtils = None,
|
||||
):
|
||||
super(AddAccountWidget, self).__init__(parent)
|
||||
self.parent = parent
|
||||
self.ui_search_content = ui_search_content
|
||||
|
||||
# align with the parent widget size
|
||||
self.resize(
|
||||
parent.frameSize().width(),
|
||||
parent.frameSize().height(),
|
||||
)
|
||||
|
||||
self._add_background()
|
||||
|
||||
self.layout = QStackedLayout()
|
||||
self.layout.addWidget(self.background)
|
||||
|
||||
self._fill_message_card(label_text)
|
||||
|
||||
content = QWidget()
|
||||
content.layout = QVBoxLayout(self)
|
||||
content.layout.setContentsMargins(0, 0, 0, 0)
|
||||
content.layout.setAlignment(Qt.AlignCenter)
|
||||
content.layout.addWidget(self._message_card)
|
||||
|
||||
self.layout.addWidget(content)
|
||||
|
||||
def _create_widget_label(self, label_text: str, props: str = ""):
|
||||
|
||||
label = QLabel(label_text)
|
||||
|
||||
# for some reason, "margin-left" doesn't make any effect here
|
||||
label.setStyleSheet(
|
||||
"QLabel {"
|
||||
+ f"{ZERO_MARGIN_PADDING}padding-left:{int(WIDGET_SIDE_BUFFER/2)};"
|
||||
+ f"padding-top:{int(WIDGET_SIDE_BUFFER/4)}; margin-bottom:{int(WIDGET_SIDE_BUFFER/4)};"
|
||||
+ f"text-align:left;{props}"
|
||||
+ "}"
|
||||
)
|
||||
return label
|
||||
|
||||
def _create_text_widget(self, label_text: str, props: str = ""):
|
||||
|
||||
label = QLabel(label_text)
|
||||
|
||||
# for some reason, "margin-left" doesn't make any effect here
|
||||
label.setStyleSheet(
|
||||
"QLabel {"
|
||||
+ f"{ZERO_MARGIN_PADDING}padding-left:5px;padding-right:5px;padding-bottom:5px;"
|
||||
+ f"text-align:left;{props}"
|
||||
+ "}"
|
||||
)
|
||||
return label
|
||||
|
||||
def _add_background(self):
|
||||
self.background = BackgroundWidget(parent=self, transparent=False)
|
||||
self.background.show()
|
||||
|
||||
def _add_drop_shadow(self, item=None):
|
||||
if not item:
|
||||
item = self
|
||||
# create drop shadow effect
|
||||
self._shadow_effect = QGraphicsDropShadowEffect()
|
||||
self._shadow_effect.setOffset(2, 2)
|
||||
self._shadow_effect.setBlurRadius(8)
|
||||
self._shadow_effect.setColor(QColor.fromRgb(100, 100, 100, 150))
|
||||
|
||||
item.setGraphicsEffect(self._shadow_effect)
|
||||
|
||||
def _fill_message_card(self, label_text: str):
|
||||
|
||||
self._message_card = QWidget()
|
||||
self._message_card.setAttribute(Qt.WA_StyledBackground, True)
|
||||
self._message_card.setStyleSheet(
|
||||
"QWidget {" + "border-radius: 10px;" + f"{BACKGR_COLOR_WHITE}" + "}"
|
||||
)
|
||||
boxLayout = QVBoxLayout(self._message_card)
|
||||
|
||||
label_main = self._create_widget_label(label_text)
|
||||
boxLayout.addWidget(label_main)
|
||||
|
||||
# add text 1
|
||||
label = self._create_text_widget("Server URL:")
|
||||
boxLayout.addWidget(label)
|
||||
|
||||
# add text input 1
|
||||
self.server_url_widget = QLineEdit("https://app.speckle.systems")
|
||||
self.server_url_widget.setMaxLength(40)
|
||||
self.server_url_widget.setStyleSheet(
|
||||
"QLineEdit { "
|
||||
+ f"{ZERO_MARGIN_PADDING}margin-left:{int(WIDGET_SIDE_BUFFER/6)};margin-right:{int(WIDGET_SIDE_BUFFER/6)};"
|
||||
+ "border: 1px solid lightgrey; height: 30px; border-radius: 5px; "
|
||||
+ "}"
|
||||
)
|
||||
boxLayout.addWidget(self.server_url_widget)
|
||||
|
||||
button_create = self._create_add_button()
|
||||
boxLayout.addWidget(button_create)
|
||||
|
||||
self._add_drop_shadow(self._message_card)
|
||||
|
||||
def _create_add_button(self) -> QPushButton:
|
||||
|
||||
button_create = QPushButton("Create")
|
||||
button_create.setStyleSheet(
|
||||
"QPushButton {"
|
||||
+ f"color:white;border-radius: 7px;margin:5px;padding: 5px;height: 20px;text-align: center;{BACKGR_COLOR}"
|
||||
+ "} QPushButton:hover { "
|
||||
+ f"{BACKGR_COLOR_LIGHT};"
|
||||
+ " }"
|
||||
)
|
||||
button_create.clicked.connect(self._add_account)
|
||||
button_create.clicked.connect(lambda: self._create_ready_button(button_create))
|
||||
|
||||
return button_create
|
||||
|
||||
def _create_ready_button(self, button):
|
||||
|
||||
try:
|
||||
button.clicked.disconnect(self._add_account)
|
||||
button.clicked.disconnect(self._create_ready_button)
|
||||
except:
|
||||
pass # ignore if methods already disconnected
|
||||
|
||||
button.setText("READY!")
|
||||
button.clicked.connect(self._exit_widget)
|
||||
|
||||
button.setStyleSheet(
|
||||
"QPushButton {"
|
||||
+ f"color:white;border-radius: 7px;margin:5px;padding: 5px;height: 20px;text-align: center;{BACKGR_COLOR}"
|
||||
+ "} QPushButton:hover { "
|
||||
+ f"{BACKGR_COLOR_LIGHT};"
|
||||
+ " }"
|
||||
)
|
||||
|
||||
def _exit_widget(self):
|
||||
|
||||
# the next signal will trigger closing the widget and refreshing project list
|
||||
self.ui_search_content.add_new_account_signal.emit()
|
||||
|
||||
def _add_account(self):
|
||||
|
||||
# create a new account, authenticate and write to DB
|
||||
server_url: str = self.server_url_widget.text()
|
||||
|
||||
# Logic to handle sign in
|
||||
api_url = "http://localhost:29364"
|
||||
url = f"{api_url}/auth/add-account?serverUrl={server_url}"
|
||||
webbrowser.open(url)
|
||||
|
||||
def resizeEvent(self, event=None):
|
||||
QWidget.resizeEvent(self, event)
|
||||
try:
|
||||
self.background.resize(
|
||||
self.parent.frameSize().width(),
|
||||
self.parent.frameSize().height(),
|
||||
)
|
||||
|
||||
self._message_card.setGeometry(
|
||||
int(0.5 * WIDGET_SIDE_BUFFER),
|
||||
int(
|
||||
(self.parent.frameSize().height() - self._message_card.height()) / 2
|
||||
),
|
||||
self.parent.frameSize().width() - 1 * WIDGET_SIDE_BUFFER,
|
||||
self._message_card.height(),
|
||||
)
|
||||
except RuntimeError as e:
|
||||
# e.g. Widget was deleted
|
||||
pass
|
||||
@@ -17,9 +17,9 @@ from speckle.ui.widgets.widget_card_from_list import CardInListWidget
|
||||
class CardsListTemporaryWidget(QWidget):
|
||||
|
||||
background: BackgroundWidget = None
|
||||
scroll_area: QtWidgets.QScrollArea = None
|
||||
cards_list_widget: QWidget = None # needed here to resize child elements
|
||||
load_more_btn: QPushButton = None
|
||||
scroll_area: QtWidgets.QScrollArea = None
|
||||
|
||||
scroll_container: QWidget = None # overall container, added after the label
|
||||
|
||||
@@ -29,9 +29,11 @@ class CardsListTemporaryWidget(QWidget):
|
||||
parent=None,
|
||||
label_text: str = "Label",
|
||||
cards_content_list: List[List],
|
||||
init_load_more_btn: bool = True,
|
||||
):
|
||||
super(CardsListTemporaryWidget, self).__init__(parent)
|
||||
self.parent: "SpeckleQGISv3Dialog" = parent
|
||||
self.init_load_more_btn = init_load_more_btn
|
||||
|
||||
# align with the parent widget size
|
||||
self.resize(
|
||||
@@ -82,7 +84,7 @@ class CardsListTemporaryWidget(QWidget):
|
||||
|
||||
return scroll_container
|
||||
|
||||
def _create_container(self):
|
||||
def _create_container(self) -> QWidget:
|
||||
|
||||
scroll_container = QWidget()
|
||||
scroll_container.setAttribute(QtCore.Qt.WA_StyledBackground, True)
|
||||
@@ -114,8 +116,8 @@ class CardsListTemporaryWidget(QWidget):
|
||||
self.scroll_area.setAlignment(Qt.AlignHCenter)
|
||||
|
||||
# create a widget inside scroll area
|
||||
cards_list_widget = self._create_area_with_cards(cards_content_list)
|
||||
self.scroll_area.setWidget(cards_list_widget)
|
||||
self.cards_list_widget = self._create_area_with_cards(cards_content_list)
|
||||
self.scroll_area.setWidget(self.cards_list_widget)
|
||||
|
||||
return self.scroll_area
|
||||
|
||||
@@ -155,22 +157,21 @@ class CardsListTemporaryWidget(QWidget):
|
||||
|
||||
def _create_area_with_cards(self, cards_content_list: List[List]) -> QWidget:
|
||||
|
||||
self.cards_list_widget = QWidget()
|
||||
self.cards_list_widget.setStyleSheet(
|
||||
"QWidget {" + f"{ZERO_MARGIN_PADDING}" + "}"
|
||||
)
|
||||
_ = QVBoxLayout(self.cards_list_widget)
|
||||
cards_list_widget = QWidget()
|
||||
cards_list_widget.setStyleSheet("QWidget {" + f"{ZERO_MARGIN_PADDING}" + "}")
|
||||
_ = QVBoxLayout(cards_list_widget)
|
||||
|
||||
# in case the input argument was missing or None, don't create any cards
|
||||
if isinstance(cards_content_list, list):
|
||||
for content in cards_content_list:
|
||||
project_card = CardInListWidget(content)
|
||||
self.cards_list_widget.layout().addWidget(project_card)
|
||||
cards_list_widget.layout().addWidget(project_card)
|
||||
|
||||
self._create_load_more_btn()
|
||||
self.cards_list_widget.layout().addWidget(self.load_more_btn)
|
||||
if self.init_load_more_btn:
|
||||
self._create_load_more_btn()
|
||||
cards_list_widget.layout().addWidget(self.load_more_btn)
|
||||
|
||||
return self.cards_list_widget
|
||||
return cards_list_widget
|
||||
|
||||
def _add_more_cards(
|
||||
self, new_cards_content_list: list, keep_scroll_on_top=False, batch_size=1
|
||||
@@ -188,6 +189,7 @@ class CardsListTemporaryWidget(QWidget):
|
||||
assigned_cards_list_widget = self._create_area_with_cards(existing_content)
|
||||
|
||||
self.scroll_area.setWidget(assigned_cards_list_widget)
|
||||
self.cards_list_widget = assigned_cards_list_widget
|
||||
|
||||
# scroll down
|
||||
if not keep_scroll_on_top:
|
||||
@@ -195,9 +197,8 @@ class CardsListTemporaryWidget(QWidget):
|
||||
vbar.setValue(vbar.maximum())
|
||||
|
||||
# style LoadMore buttom
|
||||
if len(new_cards_content_list) < batch_size:
|
||||
if self.load_more_btn and len(new_cards_content_list) < batch_size:
|
||||
self._style_load_btn(active=False, text="No more items found")
|
||||
return
|
||||
|
||||
def _remove_all_cards(self):
|
||||
all_count = self.cards_list_widget.layout().count()
|
||||
|
||||
@@ -28,7 +28,6 @@ class NewProjectWidget(QWidget):
|
||||
None # needs to be here, so it can be called on resize event
|
||||
)
|
||||
project_name_widget: QLineEdit = None
|
||||
workspace_widget: QLineEdit = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -131,22 +130,6 @@ class NewProjectWidget(QWidget):
|
||||
)
|
||||
boxLayout.addWidget(self.project_name_widget)
|
||||
|
||||
# add text 2
|
||||
label2 = self._create_text_widget("Workspaces:")
|
||||
label2.setEnabled(False)
|
||||
boxLayout.addWidget(label2)
|
||||
|
||||
# add text input 2
|
||||
self.workspace_widget = QLineEdit()
|
||||
self.workspace_widget.setStyleSheet(
|
||||
"QLineEdit { "
|
||||
+ f"{ZERO_MARGIN_PADDING}margin-left:{int(WIDGET_SIDE_BUFFER/6)};margin-right:{int(WIDGET_SIDE_BUFFER/6)};"
|
||||
+ "border: 1px solid lightgrey; height: 30px; border-radius: 5px; "
|
||||
+ "}"
|
||||
)
|
||||
self.workspace_widget.setEnabled(False)
|
||||
boxLayout.addWidget(self.workspace_widget)
|
||||
|
||||
button_create = self._create_create_button()
|
||||
boxLayout.addWidget(button_create)
|
||||
|
||||
@@ -154,20 +137,27 @@ class NewProjectWidget(QWidget):
|
||||
|
||||
def _create_create_button(self) -> QPushButton:
|
||||
|
||||
button_publish = QPushButton("Create")
|
||||
button_publish.clicked.connect(self._create_project_and_exit_widget)
|
||||
button_publish.setStyleSheet(
|
||||
button_create = QPushButton("Create")
|
||||
button_create.clicked.connect(self._create_project_and_exit_widget)
|
||||
button_create.setStyleSheet(
|
||||
"QPushButton {"
|
||||
+ f"color:white;border-radius: 7px;margin:5px;padding: 5px;height: 20px;text-align: center;{BACKGR_COLOR}"
|
||||
+ "} QPushButton:hover { "
|
||||
+ f"{BACKGR_COLOR_LIGHT};"
|
||||
+ " }"
|
||||
)
|
||||
return button_publish
|
||||
return button_create
|
||||
|
||||
def _create_project_and_exit_widget(self):
|
||||
|
||||
self.ui_search_content.create_new_project(self.project_name_widget.text(), None)
|
||||
workspace_id = (
|
||||
self.ui_search_content.current_workspace.id
|
||||
if self.ui_search_content.current_workspace
|
||||
else None
|
||||
)
|
||||
self.ui_search_content.create_new_project(
|
||||
self.project_name_widget.text(), workspace_id
|
||||
)
|
||||
|
||||
# the next signal will trigger closing the widget and refreshing project list
|
||||
self.ui_search_content.change_account_and_projects_signal.emit()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
from speckle.ui.widgets.utils.global_resources import (
|
||||
BACKGR_COLOR,
|
||||
BACKGR_COLOR_LIGHT,
|
||||
@@ -18,6 +18,8 @@ from PyQt5.QtWidgets import (
|
||||
QWidget,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QComboBox,
|
||||
QSizePolicy,
|
||||
)
|
||||
|
||||
|
||||
@@ -26,6 +28,8 @@ class ProjectSearchWidget(CardsListTemporaryWidget):
|
||||
ui_search_content: UiSearchUtils = None
|
||||
account_switch_btn: QPushButton = None
|
||||
search_widget: QLineEdit = None
|
||||
workspaces_dropdown: QComboBox = None
|
||||
workspaces: List["Workspace"] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -39,6 +43,7 @@ class ProjectSearchWidget(CardsListTemporaryWidget):
|
||||
|
||||
# get content for project cards
|
||||
self.ui_search_content = UiSearchUtils()
|
||||
self.workspaces = self.ui_search_content.get_workspaces()
|
||||
|
||||
# customize load_more function
|
||||
self._load_more = lambda: self._add_projects(clear_cursor=False)
|
||||
@@ -49,11 +54,23 @@ class ProjectSearchWidget(CardsListTemporaryWidget):
|
||||
label_text=label_text,
|
||||
cards_content_list=[],
|
||||
)
|
||||
self._add_search_and_account_switch_line()
|
||||
self._add_project_search_and_project_add_line()
|
||||
self._add_workspace_search_and_account_switch_line()
|
||||
self._add_projects(clear_cursor=True)
|
||||
|
||||
def _add_projects(self, clear_cursor=False, name_filter: Optional[str] = None):
|
||||
|
||||
if self.ui_search_content.speckle_client is None:
|
||||
return
|
||||
|
||||
workspace_id = None # default to "Personal Projects"
|
||||
index = self.workspaces_dropdown.currentIndex()
|
||||
if index < len(self.workspaces):
|
||||
workspace_id = self.workspaces[index].id
|
||||
self.ui_search_content.current_workspace = self.workspaces[index]
|
||||
else: # personal projects
|
||||
self.ui_search_content.current_workspace = None
|
||||
|
||||
if name_filter is None:
|
||||
# just get the projects in batches
|
||||
new_project_cards: list = self.ui_search_content.get_new_projects_content(
|
||||
@@ -75,15 +92,20 @@ class ProjectSearchWidget(CardsListTemporaryWidget):
|
||||
self.resizeEvent()
|
||||
|
||||
def refresh_projects(self, name_filter: Optional[str] = None):
|
||||
if self.account_switch_btn:
|
||||
self.account_switch_btn.setText(
|
||||
self.ui_search_content.get_account_initials()
|
||||
)
|
||||
|
||||
self._remove_all_cards()
|
||||
self._add_projects(clear_cursor=True, name_filter=name_filter)
|
||||
|
||||
def clear_search_bar(self):
|
||||
self.search_widget.setText("")
|
||||
|
||||
def _add_search_and_account_switch_line(self):
|
||||
def _add_project_search_and_project_add_line(self):
|
||||
|
||||
# create a line widget
|
||||
# create an empty widget
|
||||
line = QWidget()
|
||||
line.setStyleSheet(
|
||||
"QWidget {"
|
||||
@@ -104,12 +126,46 @@ class ProjectSearchWidget(CardsListTemporaryWidget):
|
||||
new_project_btn = self._create_new_project_btn()
|
||||
layout_line.addWidget(new_project_btn)
|
||||
|
||||
self.scroll_container.layout().insertWidget(1, line)
|
||||
|
||||
def _add_workspace_search_and_account_switch_line(self):
|
||||
|
||||
# create an empty widget
|
||||
line = QWidget()
|
||||
line.setStyleSheet(
|
||||
"QWidget {"
|
||||
+ f"border-radius: 0px;color:white;{ZERO_MARGIN_PADDING}"
|
||||
+ f"margin-left:{int(WIDGET_SIDE_BUFFER/4)};margin-right:{int(WIDGET_SIDE_BUFFER/4)};text-align: left;"
|
||||
+ "}"
|
||||
)
|
||||
layout_line = QHBoxLayout(line)
|
||||
layout_line.setAlignment(Qt.AlignLeft)
|
||||
layout_line.setContentsMargins(10, 0, 0, 0)
|
||||
|
||||
# workspaces selection dropdown
|
||||
self.workspaces_dropdown = QComboBox()
|
||||
self.workspaces_dropdown.currentIndexChanged.connect(self.refresh_projects)
|
||||
self._fill_workspace_dropdown()
|
||||
|
||||
layout_line.addWidget(self.workspaces_dropdown)
|
||||
|
||||
# Account switch buttom
|
||||
self.account_switch_btn = self._create_account_switch_btn()
|
||||
layout_line.addWidget(self.account_switch_btn)
|
||||
|
||||
self.scroll_container.layout().insertWidget(1, line)
|
||||
|
||||
def _fill_workspace_dropdown(self):
|
||||
|
||||
self.workspaces_dropdown.clear()
|
||||
self.workspaces_dropdown.addItems([x.name for x in self.workspaces])
|
||||
self.workspaces_dropdown.addItem("Personal Projects")
|
||||
|
||||
self.workspaces_dropdown.setStyleSheet(
|
||||
"""QComboBox { background-color: white; border: 1px solid lightgrey; border-radius: 5px; color: black; height: 30px; padding: 0px 0px 0px 10px; }"""
|
||||
)
|
||||
self.workspaces_dropdown.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
|
||||
def _create_search_widget(self):
|
||||
text_box = QLineEdit()
|
||||
text_box.setMaxLength(20)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import os.path
|
||||
from typing import Optional
|
||||
from speckle.host_apps.qgis.connectors.utils import setup_metrics
|
||||
from speckle.host_apps.qgis.qgis_module import SpeckleQGISv3Module
|
||||
|
||||
# Initialize Qt resources from file resources.py
|
||||
@@ -30,6 +31,8 @@ class SpeckleQGIS(SpeckleQGISv3Module):
|
||||
application at run time.
|
||||
:type iface: QgsInterface
|
||||
"""
|
||||
setup_metrics()
|
||||
|
||||
super(SpeckleQGIS, self).__init__(iface)
|
||||
|
||||
# initialize plugin directory
|
||||
|
||||
Reference in New Issue
Block a user