Compare commits
12 Commits
v3.0.0-alpha.41
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 237381939f | |||
| 6a8b2d9e28 | |||
| 54241d427e | |||
| 600c4460c1 | |||
| 8c49ff8ee0 | |||
| 5bd67fa171 | |||
| 0f5957cbd0 | |||
| 6cd0d47dae | |||
| 72e6566334 | |||
| 19b87984e4 | |||
| c694cf57a9 | |||
| 2f54e90cdf |
@@ -1,8 +1,8 @@
|
||||
name: build_qgis
|
||||
on:
|
||||
push:
|
||||
branches: ["main", "dev", "release/*", "alan/*"] # Continuous delivery on every long-lived branch
|
||||
tags: ["v3.*"] # Manual delivery on every 3.x tag
|
||||
branches: ["main", "installer-test/**"] # Continuous delivery on every long-lived branch
|
||||
tags: ["v3.*.*"] # Manual delivery on every v3.x tag
|
||||
|
||||
env:
|
||||
ZipName: "qgis.zip"
|
||||
@@ -75,16 +75,21 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-connector
|
||||
env:
|
||||
IS_TAG_BUILD: ${{ github.ref_type == 'tag' }}
|
||||
IS_RELEASE_BRANCH: ${{ startsWith(github.ref_name, 'release/') || github.ref_name == 'main'}}
|
||||
IS_PUBLIC_RELEASE: ${{ github.ref_type == 'tag' }}
|
||||
steps:
|
||||
- name: 🔫 Trigger Build QGIS
|
||||
uses: ALEEF02/workflow-dispatch@v3.0.0
|
||||
- name: 🔫 Trigger Build Installer(s)
|
||||
uses: the-actions-org/workflow-dispatch@v4.0.0
|
||||
with:
|
||||
workflow: Build QGIS
|
||||
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 }}", "public_release": ${{ env.IS_TAG_BUILD }} }'
|
||||
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
|
||||
|
||||
-20
@@ -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.
|
||||
@@ -30,26 +28,8 @@ try:
|
||||
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
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
annotated-types==0.7.0
|
||||
anyio==4.8.0
|
||||
anyio==4.9.0
|
||||
appdirs==1.4.4
|
||||
attrs==25.1.0
|
||||
attrs==25.3.0
|
||||
backoff==2.2.1
|
||||
certifi==2025.1.31
|
||||
charset-normalizer==3.4.1
|
||||
certifi==2025.4.26
|
||||
charset-normalizer==3.4.2
|
||||
click-plugins==1.1.1
|
||||
click==8.1.8
|
||||
click==8.2.0
|
||||
cligj==0.7.2
|
||||
colorama==0.4.6
|
||||
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.6
|
||||
h11==0.14.0
|
||||
httpcore==1.0.7
|
||||
gql==3.5.2
|
||||
graphql-core==3.2.4
|
||||
h11==0.16.0
|
||||
httpcore==1.0.9
|
||||
httpx==0.28.1
|
||||
idna==3.10
|
||||
multidict==6.1.0
|
||||
multidict==6.4.3
|
||||
numpy==1.26.4
|
||||
packaging==24.2
|
||||
packaging==25.0
|
||||
pandas==2.2.3
|
||||
propcache==0.2.1
|
||||
pydantic-core==2.27.2
|
||||
pydantic-settings==2.7.1
|
||||
pydantic==2.10.6
|
||||
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
|
||||
python-dotenv==1.0.1
|
||||
pytz==2025.1
|
||||
python-dotenv==1.1.0
|
||||
pytz==2025.2
|
||||
requests-toolbelt==1.0.0
|
||||
requests==2.31.0
|
||||
scipy==1.15.2
|
||||
shapely==2.0.7
|
||||
scipy==1.15.3
|
||||
shapely==2.1.0
|
||||
six==1.17.0
|
||||
sniffio==1.3.1
|
||||
specklepy==3.0.0a2
|
||||
stringcase==1.2.0
|
||||
typing-extensions==4.12.2
|
||||
tzdata==2025.1
|
||||
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.17.2
|
||||
yarl==1.18.3
|
||||
yarl==1.20.0
|
||||
Generated
+756
-714
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -17,7 +17,7 @@ geopandas = "0.13.2"
|
||||
geovoronoi = "0.4.0"
|
||||
scipy = "^1.13.0"
|
||||
earcut = "1.1.5"
|
||||
specklepy = {version = "^3.0.0a2", allow-prereleases = true}
|
||||
specklepy = {version = "^3.0.0a15", allow-prereleases = true}
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^7.1.3"
|
||||
|
||||
@@ -6,7 +6,7 @@ from specklepy.objects.models.collections.collection import Collection
|
||||
from specklepy.objects.proxies import ColorProxy
|
||||
|
||||
from qgis.core import QgsLayerTreeGroup, QgsVectorLayer, QgsRasterLayer, QgsFeature
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, QTimer
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, QTimer, QVariant
|
||||
|
||||
|
||||
class MetaQObject(type(QObject), type(DocumentModelStore)):
|
||||
@@ -209,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()
|
||||
@@ -223,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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -96,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),
|
||||
},
|
||||
|
||||
@@ -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,11 +160,14 @@ 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],
|
||||
role,
|
||||
f"updated {time_ago(project.updated_at)}",
|
||||
]
|
||||
content_list.append(project_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)."""
|
||||
|
||||
|
||||
+76
-15
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -221,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()
|
||||
|
||||
@@ -247,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()
|
||||
|
||||
@@ -269,6 +277,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()
|
||||
|
||||
@@ -287,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
|
||||
@@ -378,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
|
||||
)
|
||||
@@ -405,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
|
||||
@@ -413,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):
|
||||
@@ -477,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