28 Commits

Author SHA1 Message Date
KatKatKateryna bc0afeb19e remove extra input 2025-02-26 14:18:29 +00:00
Jedd Morgan 0a2809400e restructed zip 2025-02-26 14:04:54 +00:00
Jedd Morgan d456e7b5ce rename zip instead 2025-02-26 14:01:53 +00:00
Jedd Morgan 6bcd182753 don't need to setup .net on a machine that already has it 2025-02-26 13:56:03 +00:00
Jedd Morgan 9995fcb24a Merge remote-tracking branch 'origin/alan/test-ci' into alan/test-ci 2025-02-26 13:55:16 +00:00
Jedd Morgan 354be78ce8 Changed zip name to match slug 2025-02-26 13:54:56 +00:00
KatKatKateryna 62cac5d74f updates 2025-02-26 13:36:56 +00:00
KatKatKateryna 532446f8ab Merge branch 'alan/test-ci' of https://github.com/specklesystems/speckle-qgis-v3 into alan/test-ci 2025-02-20 12:25:59 +00:00
KatKatKateryna a7c0a142e7 fix version and workflow name (#25)
* Revert "dummy tag"

This reverts commit b10fdf3035.

* remove env

* trigger wrong workflow(just to check version)

* Revert "trigger wrong workflow(just to check version)"

This reverts commit 21958d40c7.
2025-02-20 20:12:24 +08:00
KatKatKateryna cfbe6b54fa Revert "dummy tag"
This reverts commit b10fdf3035.
2025-02-20 11:49:06 +00:00
KatKatKateryna b10fdf3035 dummy tag 2025-02-20 11:41:10 +00:00
Jedd Morgan de80273f32 continue on error false 2025-02-19 15:28:03 +00:00
Jedd Morgan f7aeaa851f Added dotnet tool manifest 2025-02-19 15:22:55 +00:00
Jedd Morgan 75c9f8ea3a build was gitignored 2025-02-19 15:18:43 +00:00
Jedd Morgan 205c7c5bc7 change action to run sh 2025-02-19 15:12:07 +00:00
Jedd Morgan 03ec7f23a2 Add bash script as executable 2025-02-19 15:11:44 +00:00
Jedd Morgan 7dfbd458f9 Add bash script 2025-02-19 15:11:24 +00:00
Jedd Morgan 13fb88b130 Added gitversion 2025-02-19 15:05:23 +00:00
Jedd Morgan 3484213be9 Added comment 2025-02-19 13:48:29 +00:00
Jedd Morgan 6096c017ad Pinned CI python version to 3.11 2025-02-19 13:47:36 +00:00
KatKatKateryna 63379c495c rename repo 2025-02-19 13:38:16 +00:00
Jedd Morgan 6fe8bb5f7c Added poetry install 2025-02-19 13:30:30 +00:00
Jedd Morgan 2eb52af055 Poetry export plugin 2025-02-19 13:26:18 +00:00
Jedd Morgan 5db67f496e First pass implementing QGIS github actions 2025-02-19 13:22:13 +00:00
KatKatKateryna 05aa7f9dee return patch requirements 2025-02-19 12:41:36 +00:00
KatKatKateryna 453f222a67 Merge branch 'dev' into github-actions 2025-02-19 12:25:39 +00:00
KatKatKateryna 83c1ea4005 Merge branch 'dev' into github-actions 2025-02-19 12:00:06 +00:00
KatKatKateryna dbe503be8f Create release.yml 2025-02-18 17:33:07 +08:00
29 changed files with 1016 additions and 1708 deletions
+12 -22
View File
@@ -1,8 +1,8 @@
name: build_qgis
name: build_powerbi
on:
push:
branches: ["main", "installer-test/**"] # Continuous delivery on every long-lived branch
tags: ["v3.*.*"] # Manual delivery on every v3.x tag
branches: ["main", "dev", "release/*", "alan/*"] # Continuous delivery on every long-lived branch
tags: ["v3.*"] # Manual delivery on every 3.x tag
env:
ZipName: "qgis.zip"
@@ -11,8 +11,7 @@ jobs:
build-connector:
runs-on: ubuntu-latest
outputs:
semver: ${{ steps.set-version.outputs.semver }}
file-version: ${{ steps.set-info-version.outputs.file-version }}
version: ${{ steps.set-version.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -41,10 +40,6 @@ jobs:
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
@@ -56,11 +51,11 @@ jobs:
- id: set-version
name: Set version to output
run: echo "semver=${{ env.GitVersion_FullSemVer }}" >> "$GITHUB_OUTPUT"
run: echo "version=${{ 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?
run: echo "file_version=${{ env.GitVersion_AssemblySemVer}}" >> "$GITHUB_OUTPUT" # version will be retrieved from tag?
- name: ⬆️ Upload artifacts
uses: actions/upload-artifact@v4
@@ -75,21 +70,16 @@ jobs:
runs-on: ubuntu-latest
needs: build-connector
env:
IS_PUBLIC_RELEASE: ${{ github.ref_type == 'tag' }}
IS_TAG_BUILD: ${{ github.ref_type == 'tag' }}
IS_RELEASE_BRANCH: ${{ startsWith(github.ref_name, 'release/') || github.ref_name == 'main'}}
steps:
- name: 🔫 Trigger Build Installer(s)
uses: the-actions-org/workflow-dispatch@v4.0.0
- name: 🔫 Trigger Build QGIS
uses: ALEEF02/workflow-dispatch@v3.0.0
with:
workflow: Build Installers
workflow: Build QGIS
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 }}
}'
inputs: '{ "run_id": "${{ github.run_id }}", "semver": "${{ needs.build-connector.outputs.semver }}", "file_version": "${{ needs.build-connector.outputs.file-version }}", "public_release": ${{ env.IS_TAG_BUILD }} }'
ref: main
wait-for-completion: true
wait-for-completion-interval: 10s
+21 -1
View File
@@ -11,6 +11,8 @@ 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.
@@ -25,11 +27,29 @@ try:
# Ensure dependencies are installed in the machine
startDebugger()
ensure_dependencies("QGISv3")
ensure_dependencies("QGIS")
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
+97
View File
@@ -0,0 +1,97 @@
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()
-1
View File
@@ -186,7 +186,6 @@ def install_requirements(host_application: str) -> None:
"-m",
"pip",
"install",
"--pre",
"-t",
str(path),
"-r",
-39
View File
@@ -1,39 +0,0 @@
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()
+30 -30
View File
@@ -1,51 +1,51 @@
annotated-types==0.7.0
anyio==4.9.0
anyio==4.6.2.post1
appdirs==1.4.4
attrs==25.3.0
attrs==23.2.0
backoff==2.2.1
certifi==2025.4.26
charset-normalizer==3.4.2
certifi==2024.8.30
charset-normalizer==3.4.0
click-plugins==1.1.1
click==8.2.0
click==8.1.7
cligj==0.7.2
colorama==0.4.6
deprecated==1.2.18
deprecated==1.2.14
earcut==1.1.5
exceptiongroup==1.3.0
exceptiongroup==1.2.2
fiona==1.10.1
geopandas==0.13.2
geovoronoi==0.4.0
gql==3.5.2
graphql-core==3.2.4
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
gql==3.5.0
graphql-core==3.2.5
h11==0.14.0
httpcore==1.0.6
httpx==0.25.2
idna==3.10
multidict==6.4.3
importlib-metadata==8.5.0
multidict==6.1.0
numpy==1.26.4
packaging==25.0
packaging==24.1
pandas==2.2.3
propcache==0.3.1
pydantic-core==2.33.2
pydantic-settings==2.9.1
pydantic==2.11.4
pyproj==3.7.1
propcache==0.2.0
pydantic-core==2.23.4
pydantic==2.9.2
pyproj==3.6.1
pyshp==2.3.1
python-dateutil==2.9.0.post0
python-dotenv==1.1.0
pytz==2025.2
pytz==2024.2
requests-toolbelt==1.0.0
requests==2.31.0
scipy==1.15.3
shapely==2.1.0
six==1.17.0
scipy==1.13.1
shapely==2.0.6
six==1.16.0
sniffio==1.3.1
specklepy==3.0.0a15
typing-extensions==4.13.2
typing-inspection==0.4.0
tzdata==2025.2
specklepy==2.21.3
stringcase==1.2.0
typing-extensions==4.12.2
tzdata==2024.2
ujson==5.10.0
urllib3==2.2.1
websockets==11.0.3
wrapt==1.17.2
yarl==1.20.0
wrapt==1.16.0
yarl==1.17.1
zipp==3.20.2
Generated
+700 -758
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -17,7 +17,7 @@ geopandas = "0.13.2"
geovoronoi = "0.4.0"
scipy = "^1.13.0"
earcut = "1.1.5"
specklepy = {version = "^3.0.0a15", allow-prereleases = true}
specklepy = {version = "^3.0.0a1", allow-prereleases = true}
[tool.poetry.group.dev.dependencies]
pytest = "^7.1.3"
@@ -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,6 +127,13 @@ 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
+7 -40
View File
@@ -5,38 +5,15 @@ 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 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)
class QgisDocumentStore(DocumentModelStore):
def __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
@@ -118,10 +95,7 @@ class QgisLayerUnpacker:
if isinstance(layer, QgsVectorLayer):
layer_fields: Dict[str, Any] = {}
for field in layer.fields():
# 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()
layer_fields[field.name()] = field.type()
collection["fields"] = layer_fields
collection["wkbType"] = layer.wkbType().name
@@ -209,11 +183,8 @@ class QgisColorUnpacker:
) -> Any:
feature_value_for_rendering = feature.attribute(self.stored_renderer_field)
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()
category_index = renderer.categoryIndexForValue(feature_value_for_rendering)
value_symbol = renderer.categories()[category_index].symbol()
if not value_symbol:
value_symbol = renderer.sourceSymbol()
@@ -226,11 +197,7 @@ class QgisColorUnpacker:
) -> Any:
feature_value_for_rendering = feature.attribute(self.stored_renderer_field)
if not isinstance(feature_value_for_rendering, QVariant): # for QVariant.NULL
value_symbol = renderer.symbolForValue(feature_value_for_rendering)
else:
value_symbol = renderer.sourceSymbol()
value_symbol = renderer.symbolForValue(feature_value_for_rendering)
if not value_symbol:
value_symbol = renderer.sourceSymbol()
@@ -1,7 +1,6 @@
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
@@ -235,35 +234,3 @@ 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,14 +19,12 @@ from speckle.ui.models import SendInfo
from specklepy.objects.base import Base
# from specklepy.objects.data import QgisObject
from specklepy.objects.geometry import Mesh
from specklepy.objects.geometry.mesh 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):
@@ -150,17 +148,6 @@ 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,
@@ -187,9 +174,6 @@ 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(iface)
self.document_store = QgisDocumentStore()
self.basic_binding = QgisBasicConnectorBinding(self.document_store, bridge)
self.send_binding = QgisSendBinding(
bridge=bridge,
+2 -39
View File
@@ -1,52 +1,15 @@
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, QgsApplication
from qgis.core import Qgis
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,8 +14,9 @@ from qgis.core import (
QgsRasterLayer,
QgsGeometry,
QgsCoordinateTransform,
QgsPoint,
)
from PyQt5 import QtCore
from osgeo import gdal
class DisplayValueExtractor:
@@ -92,24 +93,7 @@ class PropertiesExtractor:
def get_properties(self, core_object: Any) -> Dict[str, Any]:
if isinstance(core_object, QgsFeature):
# 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
return core_object.attributeMap()
elif isinstance(core_object, QgsRasterLayer):
return {} # TODO
@@ -1,102 +0,0 @@
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 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 specklepy.objects.geometry.point import Point
from specklepy.objects.geometry.polyline import Polyline
from specklepy.objects.geometry.mesh import Mesh
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[Base]:
def convert(self, target: QgsAbstractGeometry) -> List[Polyline]:
wkb_type = target.wkbType()
@@ -142,39 +142,18 @@ class PolygonToSpeckleConverter:
or wkb_type == QgsWkbTypes.CurvePolygonM
or wkb_type == QgsWkbTypes.CurvePolygonZM
):
all_regions = []
all_z_values = []
all_curves = []
for part in target.parts():
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]
all_curves.append(
self._polyline_converter.convert(part.exteriorRing())[0]
)
inner_loops = []
for i in range(part.numInteriorRings()):
inner_loops.append(
all_curves.append(
self._polyline_converter.convert(part.interiorRing(i))[0]
)
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
return all_curves
raise ValueError(f"Geometry of type '{type(target)}' cannot be converted")
+24 -33
View File
@@ -31,9 +31,9 @@ class SpeckleQGISv3Module:
self.dockwidget = SpeckleQGISv3Dialog(
bridge=self, basic_binding=self.connector_module.basic_binding
)
self.dockwidget.header_widget = self.dockwidget.create_header(self)
self.dockwidget.runSetup()
self.dockwidget.runSetup(self)
self.connect_dockwidget_signals()
self.connect_self_signals()
def instantiate_module_dependencies(self, iface):
@@ -59,20 +59,16 @@ class SpeckleQGISv3Module:
self.dockwidget.add_send_notification
) # Send a UI notification after Send operation
# refresh widgets if document change signal received
self.connector_module.document_store.document_changed_signal.connect(
self.dockwidget.refresh_ui
)
# 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_self_signals(self):
# signal to update UI, needs t be transferred to the main thread
self.dockwidget.activity_start_signal.connect(
self.dockwidget.add_activity_status
)
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
)
@@ -96,29 +92,24 @@ class SpeckleQGISv3Module:
on_operation_progressed: "IProgress[CardProgress]",
ct: "CancellationToken",
):
# 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.."
)
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
)
# 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
)
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)
)
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,
)
def _cancel_operation(self, model_card_id: str):
+6 -6
View File
@@ -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, CORE_VERSION
from speckle.host_apps.qgis.connectors.utils import HOST_APP_FULL_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": CORE_VERSION,
"core_version": "3.0.099",
"ui": "dui3",
"workspace_id": get_project_workspace_id(client, send_info.project_id),
},
@@ -153,11 +153,11 @@ class SendOperation:
_ = api_client.version.create(
CreateVersionInput(
object_id=obj_id,
model_id=send_info.model_id,
project_id=send_info.project_id,
objectId=obj_id,
modelId=send_info.model_id,
projectId=send_info.project_id,
message="Sent from QGIS v3",
source_application=send_info.host_application,
sourceApplication=send_info.host_application,
)
)
+1 -1
View File
@@ -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).workspace_id
workspace_id = client.project.get(project_id).workspaceId
return workspace_id
+18 -50
View File
@@ -9,7 +9,6 @@ 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,
@@ -29,7 +28,6 @@ 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)
@@ -38,24 +36,29 @@ 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__()
self.batch_size = QUERY_BATCH_SIZE
accounts: List[Account] = get_accounts()
if len(accounts) > 0:
self.speckle_client: SpeckleClient = get_authenticate_client_for_account(
accounts[0]
if len(accounts) == 0: # TODO handle no local accounts
raise SpeckleException(
"Add accounts via Speckle Desktop Manager in order to start"
)
def get_accounts_content(self) -> List[List[Any]]:
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"
)
content_list = [
[
@@ -74,13 +77,11 @@ 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 "?"
return "X"
def create_new_project(self, name: str, workspace_id: Optional[str] = None):
create_new_project_query(self.speckle_client, name, workspace_id)
@@ -89,9 +90,6 @@ 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
@@ -99,18 +97,9 @@ class UiSearchUtils(QObject):
content_list: List[List] = []
projects_resource_collection: ResourceCollection[Project] = (
get_projects_from_client(
speckle_client=self.speckle_client,
workspace_id=workspace_id,
cursor=self.cursor_projects,
speckle_client=self.speckle_client, 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(
@@ -122,27 +111,15 @@ 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(
@@ -160,15 +137,12 @@ 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,
role,
f"updated {time_ago(project.updated_at)}",
project.role.split(":")[-1],
f"updated {time_ago(project.updatedAt)}",
]
content_list.append(project_content)
return content_list
@@ -231,7 +205,7 @@ class UiSearchUtils(QObject):
model_content = [
partial(self.add_selection_filter_widget, project, model),
model.name,
f"updated {time_ago(model.updated_at)}",
f"updated {time_ago(model.updatedAt)}",
project,
]
content_list.append(model_content)
@@ -257,12 +231,6 @@ 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)."""
+16 -77
View File
@@ -9,10 +9,8 @@ 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,
@@ -51,51 +49,19 @@ def get_authenticate_client_for_account(account: Account) -> SpeckleClient:
def get_projects_from_client(
speckle_client: SpeckleClient,
workspace_id: Optional[str],
cursor=None,
filter_keyword: Optional[str] = None,
speckle_client: SpeckleClient, 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:
# 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,
)
)
# 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
),
)
if not isinstance(results, ResourceCollection):
# TODO: handle
@@ -105,20 +71,6 @@ 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
@@ -127,7 +79,7 @@ def get_models_from_client(
project: Project,
cursor=None,
filter_keyword: Optional[str] = None,
) -> ResourceCollection[Model]:
) -> ResourceCollection[Project]:
results = []
if speckle_client is not None:
@@ -178,25 +130,12 @@ def create_new_project_query(
result = None
if speckle_client is not 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
)
# possible GraphQLException
result: Project = speckle_client.project.create(
input=ProjectCreateInput(
name=project_name, description=None, visibility=None
)
)
if not isinstance(result, Project):
# TODO: handle
@@ -218,7 +157,7 @@ def create_new_model_query(
# possible GraphQLException
result: Project = speckle_client.model.create(
input=CreateModelInput(
name=model_name, description=None, project_id=project_id
name=model_name, description=None, projectId=project_id
)
)
+11 -73
View File
@@ -5,7 +5,6 @@ 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
@@ -56,7 +55,6 @@ 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
@@ -82,7 +80,7 @@ class SpeckleQGISv3Dialog(QDockWidget):
self.basic_binding = basic_binding
self.bridge = bridge
def runSetup(self):
def runSetup(self, plugin):
self.placeholder_widget = QWidget()
self.placeholder_widget.layout = QVBoxLayout(self.placeholder_widget)
self.placeholder_widget.layout.setContentsMargins(0, 0, 0, 0)
@@ -90,10 +88,11 @@ class SpeckleQGISv3Dialog(QDockWidget):
self.placeholder_widget.setStyleSheet(f"{ZERO_MARGIN_PADDING}")
self.layout().addWidget(self.placeholder_widget)
# add header widget
# create and add header widget
self.header_widget = self._create_header(plugin)
self.placeholder_widget.layout.addWidget(self.header_widget)
# create and add main widget
# cerate and add main widget
self.main_widget = QWidget()
self.main_widget.layout = QStackedLayout(self.main_widget)
self.main_widget.layout.setStackingMode(QStackedLayout.StackAll)
@@ -102,13 +101,9 @@ class SpeckleQGISv3Dialog(QDockWidget):
self.placeholder_widget.layout.addWidget(self.main_widget)
# add first widget to main
self._add_start_widget()
self._add_start_widget(plugin)
def refresh_ui(self):
self._remove_all_widgets()
self._add_start_widget()
def create_header(self, plugin):
def _create_header(self, plugin):
try:
header_widget = QWidget()
header_widget.setStyleSheet(f"{BACKGR_COLOR}{ZERO_MARGIN_PADDING}")
@@ -185,7 +180,7 @@ class SpeckleQGISv3Dialog(QDockWidget):
except Exception as e:
print(e)
def _add_start_widget(self):
def _add_start_widget(self, plugin):
# document in QGIS is opened by default, we don't need as actually saved file to start working with data
document_open = True
@@ -223,9 +218,6 @@ 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()
@@ -233,7 +225,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()
@@ -252,9 +244,6 @@ 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()
@@ -262,7 +251,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:
@@ -272,14 +261,11 @@ 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()
@@ -298,10 +284,6 @@ 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
@@ -310,7 +292,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
@@ -393,10 +375,6 @@ 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
)
@@ -424,16 +402,6 @@ 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
@@ -442,13 +410,6 @@ 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):
@@ -513,29 +474,6 @@ 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:
+7 -36
View File
@@ -3,13 +3,6 @@ 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):
@@ -20,47 +13,25 @@ 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=[],
init_load_more_btn=False,
parent=parent, label_text=label_text, cards_content_list=[]
)
self.refresh_accounts()
button_create = self._create_add_button()
self.scroll_container.layout().addWidget(button_create)
self._add_accounts(clear_cursor=True)
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):
def _add_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
)
-207
View File
@@ -1,207 +0,0 @@
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,11 +29,9 @@ 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(
@@ -84,7 +82,7 @@ class CardsListTemporaryWidget(QWidget):
return scroll_container
def _create_container(self) -> QWidget:
def _create_container(self):
scroll_container = QWidget()
scroll_container.setAttribute(QtCore.Qt.WA_StyledBackground, True)
@@ -116,8 +114,8 @@ class CardsListTemporaryWidget(QWidget):
self.scroll_area.setAlignment(Qt.AlignHCenter)
# create a widget inside scroll area
self.cards_list_widget = self._create_area_with_cards(cards_content_list)
self.scroll_area.setWidget(self.cards_list_widget)
cards_list_widget = self._create_area_with_cards(cards_content_list)
self.scroll_area.setWidget(cards_list_widget)
return self.scroll_area
@@ -157,21 +155,22 @@ class CardsListTemporaryWidget(QWidget):
def _create_area_with_cards(self, cards_content_list: List[List]) -> QWidget:
cards_list_widget = QWidget()
cards_list_widget.setStyleSheet("QWidget {" + f"{ZERO_MARGIN_PADDING}" + "}")
_ = QVBoxLayout(cards_list_widget)
self.cards_list_widget = QWidget()
self.cards_list_widget.setStyleSheet(
"QWidget {" + f"{ZERO_MARGIN_PADDING}" + "}"
)
_ = QVBoxLayout(self.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)
cards_list_widget.layout().addWidget(project_card)
self.cards_list_widget.layout().addWidget(project_card)
if self.init_load_more_btn:
self._create_load_more_btn()
cards_list_widget.layout().addWidget(self.load_more_btn)
self._create_load_more_btn()
self.cards_list_widget.layout().addWidget(self.load_more_btn)
return cards_list_widget
return self.cards_list_widget
def _add_more_cards(
self, new_cards_content_list: list, keep_scroll_on_top=False, batch_size=1
@@ -189,7 +188,6 @@ 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:
@@ -197,8 +195,9 @@ class CardsListTemporaryWidget(QWidget):
vbar.setValue(vbar.maximum())
# style LoadMore buttom
if self.load_more_btn and len(new_cards_content_list) < batch_size:
if 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()
+22 -12
View File
@@ -28,6 +28,7 @@ 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,
@@ -130,6 +131,22 @@ 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)
@@ -137,27 +154,20 @@ class NewProjectWidget(QWidget):
def _create_create_button(self) -> QPushButton:
button_create = QPushButton("Create")
button_create.clicked.connect(self._create_project_and_exit_widget)
button_create.setStyleSheet(
button_publish = QPushButton("Create")
button_publish.clicked.connect(self._create_project_and_exit_widget)
button_publish.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
return button_publish
def _create_project_and_exit_widget(self):
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
)
self.ui_search_content.create_new_project(self.project_name_widget.text(), None)
# the next signal will trigger closing the widget and refreshing project list
self.ui_search_content.change_account_and_projects_signal.emit()
+4 -60
View File
@@ -1,4 +1,4 @@
from typing import List, Optional
from typing import Optional
from speckle.ui.widgets.utils.global_resources import (
BACKGR_COLOR,
BACKGR_COLOR_LIGHT,
@@ -18,8 +18,6 @@ from PyQt5.QtWidgets import (
QWidget,
QLineEdit,
QPushButton,
QComboBox,
QSizePolicy,
)
@@ -28,8 +26,6 @@ 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,
@@ -43,7 +39,6 @@ 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)
@@ -54,23 +49,11 @@ class ProjectSearchWidget(CardsListTemporaryWidget):
label_text=label_text,
cards_content_list=[],
)
self._add_project_search_and_project_add_line()
self._add_workspace_search_and_account_switch_line()
self._add_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(
@@ -92,20 +75,15 @@ 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_project_search_and_project_add_line(self):
def _add_search_and_account_switch_line(self):
# create an empty widget
# create a line widget
line = QWidget()
line.setStyleSheet(
"QWidget {"
@@ -126,46 +104,12 @@ 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)
-3
View File
@@ -2,7 +2,6 @@
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
@@ -31,8 +30,6 @@ class SpeckleQGIS(SpeckleQGISv3Module):
application at run time.
:type iface: QgsInterface
"""
setup_metrics()
super(SpeckleQGIS, self).__init__(iface)
# initialize plugin directory