From 7ae5bbf68066935164f62f9c5ea267661da93a56 Mon Sep 17 00:00:00 2001 From: KatKatKateryna Date: Sun, 30 Apr 2023 01:37:44 +0200 Subject: [PATCH] main UI window --- .gitignore | 125 ++++++ icons/cube-receive-black.png | Bin 0 -> 2920 bytes icons/cube-receive-blue.png | Bin 0 -> 3029 bytes icons/cube-receive.png | Bin 0 -> 2296 bytes icons/cube-send-black.png | Bin 0 -> 3243 bytes icons/cube-send-blue.png | Bin 0 -> 3302 bytes icons/cube-send.png | Bin 0 -> 2528 bytes icons/delete-blue.png | Bin 0 -> 1792 bytes icons/delete.png | Bin 0 -> 345 bytes icons/logo-slab-white@0.5x.png | Bin 0 -> 2068 bytes icons/magnify.png | Bin 0 -> 7294 bytes icons/size-xxl.png | Bin 0 -> 400 bytes main_dialog.py | 710 +++++++++++++++++++++++++++++++++ main_dialog_for_mainWindow.py | 647 ++++++++++++++++++++++++++++++ main_dialog_qDockwidget.ui | 312 +++++++++++++++ main_dialog_qMainWindow.ui | 316 +++++++++++++++ 16 files changed, 2110 insertions(+) create mode 100644 .gitignore create mode 100644 icons/cube-receive-black.png create mode 100644 icons/cube-receive-blue.png create mode 100644 icons/cube-receive.png create mode 100644 icons/cube-send-black.png create mode 100644 icons/cube-send-blue.png create mode 100644 icons/cube-send.png create mode 100644 icons/delete-blue.png create mode 100644 icons/delete.png create mode 100644 icons/logo-slab-white@0.5x.png create mode 100644 icons/magnify.png create mode 100644 icons/size-xxl.png create mode 100644 main_dialog.py create mode 100644 main_dialog_for_mainWindow.py create mode 100644 main_dialog_qDockwidget.ui create mode 100644 main_dialog_qMainWindow.ui diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d88d1b --- /dev/null +++ b/.gitignore @@ -0,0 +1,125 @@ +./idea/ +/.vscode/ +/speckle_qgis_dialog_base.ui.autosave + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Jetbrains stuff: +.idea + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# other +scratch.py +settings.json +**/.DS_Store +zip_build +.qt_for_python +speckle-sharp-ci-tools* +zip-build* + +/typings/* +*.csv +*.json + diff --git a/icons/cube-receive-black.png b/icons/cube-receive-black.png new file mode 100644 index 0000000000000000000000000000000000000000..a64d9e263c12482eb9d796798c39ded61d85ca41 GIT binary patch literal 2920 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 V$Z6F#sM`M+biW|VVc(7Fe*-*P;e-GH literal 0 HcmV?d00001 diff --git a/icons/cube-send-blue.png b/icons/cube-send-blue.png new file mode 100644 index 0000000000000000000000000000000000000000..aca80913c24139d1aa50f903c869ad620e67227a GIT binary patch literal 3302 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 literal 0 HcmV?d00001 diff --git a/icons/delete.png b/icons/delete.png new file mode 100644 index 0000000000000000000000000000000000000000..027374586ff3f1d7ef4040a69a395c2b392a9ddb GIT binary patch literal 345 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjEa{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZKG?d}4kf#9d}?s_1_ zS>O>_45Sml_(QhSc_1U&)5S5w<9Kp{1ZxOG5f2Yfxuk@|zr_#RflRL!6XQn$|NWOG z8=Og&dhuYPgu$bU%BC|m%yRv#Ahk8&N>dh64!{5l*E!$tK_0oAjM#0U}UUoV5no(r~EPRZ+b zALI*$77YbGvvczv$1{!Cange?uITD$Bod~kecVxJ#hX5>R_$~DT2nHmD^S-cJff(Y zAv_@*UNuyUw}so3Y&j^Z^LF+X*7b-pb+2{Bk zV(ao%SFpPW-E!W&_zi99XXyxuNzVWP010qNS#tmY1&IIv1&IM|Xr1~100uZoL_t(& zf$f=hj8#Px#(y6RC~bjgM2cOEqSyi!VmB6yiuwnF8Vez6G@?cg(F8FjnAoF6qETbe zKw<+aiqTk6*M^N)&=?T0gN79r1eVV~X70Yb_r3eree1(T_Dd%By_q?4XU=!-nR8~K z1MLd>uP3lA!}j(-ZMt+2QUPptqoBmzWR%*%wpyTTcI<765Zm3UT!BejFQ7}c=D1Z5 zV!J1mD=;a&fX-Wb?yW`D2(c|7tH?^MvlmcPt$DUL6cG{?*^9~*nAG;UXss-tYz(OkJb*-Bj57C<8fMGgf11U>`|oK{BeZs1W01gw$dq|x?0z_q0W zI|E1j8#8aGXdozXA+Q~=D=?Dgd&%>_6Trd1&8hnDfnkzX*uE5a7+4_b9NU8_Psk6F zzE1ww?ruBnMF!>xwzo}g1HqzdfIgYFMuO$lXUeMhYyiGU^?x3?T+*-UL2M5Jo&pYt zRf{Ceimp1r8emi;<9Oh~N)}`LLf|o4q33E6vI%$%cnkO%*aa8@T$Fk52?RyfOInvy z+U^gG0q!Iav{BN;yqDQdhobW(ogM4@0v7<^0nVo9IHFX6c|{WgcL zt(DE|{5`OgU@5EPGbw8c)|fm`2F}gMHvnTL&B+dI`w8G_pkJ(7Ch0fZvYiAID<%C5 z+-v(!;PK2DQ-Hw~2NV##q$^{;vA`ohkJz>dI4~-^4mi5R+NVf*)^;CY9?&;OM^!-{ zA+v#(2#Vbt9s+&^CZy`Fmo&%rp1|e6;lL8$MoDkkeh|1dGtNxltoVDa?K>q+CXn}V zrtfHA@0|H8m9!-Oo{_C{c*O#R2eE0S#cfw-PFLDIJbSJ?#Hz01jz^KYGi?V~kq7PgbH zZWFLF{;th*8*Y091$R{rizThMT|;nBX}n2LVA}GIA?!!>vCoc_4&MirdYPo9aUHt? z7q?)3n?(!=uA#JREs1Ju>Yae&fVsdKz-0v2a3ruF@J^20dIFL4z`KOa9N;iQp7eLp zQPTLVusEweqE;6mG~pS^%HTO&}dC~K8At+Ty5Fr!5MNx%S_TV2-0H0I3hIp94)Fmh6=emHQY?U9n^+8zf?V+$wA)gojN zupmYbBBZSV)V0$84kdU_}oo;&ou#Cb! zwp}CXC);y?vr}y|A{qS%S$m}z0W?I#G{N>C1h+TU_7X{-058On^ce-*pONcEIN?s? zj_5{L5?tRZ;1Ju>$-lTtZUAqJrPZhgA+?0f@+IIr!r|q?goC+-vCs9iVY8IUsmaS32*OAdis#z=}4EJJa@fNi%>q zVoA=G?f}wZ>p`~nAm@G{g|0C{Z0QZ42pN4*^wyrCR6>`Flo54T#)}dbQWG)y00000dd_W literal 0 HcmV?d00001 diff --git a/icons/magnify.png b/icons/magnify.png new file mode 100644 index 0000000000000000000000000000000000000000..ec33a636392875570f3c35407f2af282a0934586 GIT binary patch literal 7294 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 literal 0 HcmV?d00001 diff --git a/icons/size-xxl.png b/icons/size-xxl.png new file mode 100644 index 0000000000000000000000000000000000000000..33a45e75beb73439510137118161947b65c8c216 GIT binary patch literal 400 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjEa{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZKG?d}4kf#9d}?s_1_ zS>O>_45Sml_(QhSc_3qwr;B5V$MLt54{{zh;Bk%jS~qt=a>cA?^Uhsa_Pu20nu(l7 z4Ml;D7M*L&M5I12A9R=hsVwchut0;&p<#jQt<#l@wtD4$32u*Ga4GKjhe;uB^BWC6 z=^vQ#G3DtRU%9lWyDxjk1^O5;AAG6G?04@;ekXID+jjYi*?;~_e9P7=cZFI1YIys1 zw?$DKzAsK?=4!Q1Wt;uluJT2paU9TfswJ)wB`Jv|saDBFsX&Us$iT>0*T7KM&^*M@ uz{=Rj%G6NXz`)ADpzNBK8;XY9{FKbJO57UQyC;FdkHOQ`&t;ucLK6U+nuG5E literal 0 HcmV?d00001 diff --git a/main_dialog.py b/main_dialog.py new file mode 100644 index 0000000..696e379 --- /dev/null +++ b/main_dialog.py @@ -0,0 +1,710 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + SpeckleQGISDialog + A QGIS plugin + SpeckleQGIS Description + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2021-08-04 + git sha : $Format:%H$ + copyright : (C) 2021 by Speckle Systems + email : alan@speckle.systems + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" + +import inspect +import os +import threading +from plugin_utils.helpers import splitTextIntoLines +from speckle.automation.mapping_send import MappingSendDialog +from speckle.converter.layers import getAllLayers, getLayers +from speckle.DataStorage import DataStorage +from ui.LogWidget import LogWidget +from ui.logger import logToUser +#from speckle_qgis import SpeckleQGIS +import ui.speckle_qgis_dialog +from qgis.core import Qgis, QgsProject,QgsVectorLayer, QgsRasterLayer, QgsIconUtils +from specklepy.logging.exceptions import (SpeckleException, GraphQLException) +from qgis.PyQt import QtWidgets, uic +from qgis.PyQt import QtGui +from qgis.PyQt.QtGui import QIcon, QPixmap +from qgis.PyQt.QtWidgets import QCheckBox, QListWidgetItem, QAction, QDockWidget, QVBoxLayout, QHBoxLayout, QWidget, QLabel +from qgis.PyQt import QtCore +from qgis.PyQt.QtCore import pyqtSignal, Qt +from speckle.logging import logger +from specklepy.api.credentials import get_local_accounts + +from specklepy.api.wrapper import StreamWrapper +from specklepy.api.client import SpeckleClient +from specklepy.logging import metrics + +from ui.validation import tryGetStream + +# This loads your .ui file so that PyQt can populate your plugin with the elements from Qt Designer +FORM_CLASS, _ = uic.loadUiType( + os.path.join(os.path.dirname(__file__), "main_dialog_qDockwidget.ui") +) + +COLOR_HIGHLIGHT = (210,210,210) + +SPECKLE_COLOR = (59,130,246) +SPECKLE_COLOR_LIGHT = (69,140,255) +ICON_LOGO = os.path.dirname(os.path.abspath(__file__)) + "/icons/logo-slab-white@0.5x.png" + +ICON_SEARCH = os.path.dirname(os.path.abspath(__file__)) + "/icons/magnify.png" + +ICON_DELETE = os.path.dirname(os.path.abspath(__file__)) + "/icons/delete.png" +ICON_DELETE_BLUE = os.path.dirname(os.path.abspath(__file__)) + "/icons/delete-blue.png" + +ICON_SEND = os.path.dirname(os.path.abspath(__file__)) + "/icons/cube-send.png" +ICON_RECEIVE = os.path.dirname(os.path.abspath(__file__)) + "/icons/cube-receive.png" + +ICON_SEND_BLACK = os.path.dirname(os.path.abspath(__file__)) + "/icons/cube-send-black.png" +ICON_RECEIVE_BLACK = os.path.dirname(os.path.abspath(__file__)) + "/icons/cube-receive-black.png" + +ICON_SEND_BLUE = os.path.dirname(os.path.abspath(__file__)) + "/icons/cube-send-blue.png" +ICON_RECEIVE_BLUE = os.path.dirname(os.path.abspath(__file__)) + "/icons/cube-receive-blue.png" + +COLOR = f"color: rgb{str(SPECKLE_COLOR)};" +BACKGR_COLOR = f"background-color: rgb{str(SPECKLE_COLOR)};" +BACKGR_COLOR_LIGHT = f"background-color: rgb{str(SPECKLE_COLOR_LIGHT)};" + +class MainDialog(QtWidgets.QDockWidget, FORM_CLASS): + + closingPlugin = pyqtSignal() + streamList: QtWidgets.QComboBox + sendModeButton: QtWidgets.QPushButton + receiveModeButton: QtWidgets.QPushButton + streamBranchDropdown: QtWidgets.QComboBox + layerSendModeDropdown: QtWidgets.QComboBox + commitDropdown: QtWidgets.QComboBox + layersWidget: QtWidgets.QListWidget + saveLayerSelection: QtWidgets.QPushButton + runButton: QtWidgets.QPushButton + setMapping: QtWidgets.QPushButton + experimental: QCheckBox + msgLog: LogWidget = None + dataStorage: DataStorage = None + mappingSendDialog = None + + def __init__(self, parent=None): + """Constructor.""" + super(MainDialog, self).__init__(parent) + # Set up the user interface from Designer through FORM_CLASS. + # After self.setupUi() you can access any designer object by doing + # self., and you can use autoconnect slots - see + # http://qt-project.org/doc/qt-4.8/designer-using-a-ui-file.html + # #widgets-and-dialogs-with-auto-connect + self.setupUi(self) + + self.streamBranchDropdown.setMaxCount(100) + self.commitDropdown.setMaxCount(100) + + self.streams_add_button.setFlat(True) + self.streams_remove_button.setFlat(True) + self.saveSurveyPoint.setFlat(True) + self.saveLayerSelection.setFlat(True) + self.reloadButton.setFlat(True) + self.closeButton.setFlat(True) + + # https://stackoverflow.com/questions/67585501/pyqt-how-to-use-hover-in-button-stylesheet + #color = f"color: rgb{str(SPECKLE_COLOR)};" + #backgr_color = f"background-color: rgb{str(SPECKLE_COLOR)};" + #backgr_color_light = f"background-color: rgb{str(SPECKLE_COLOR_LIGHT)};" + backgr_image_del = f"border-image: url({ICON_DELETE_BLUE});" + self.streams_add_button.setIcon(QIcon(ICON_SEARCH)) + self.streams_add_button.setMaximumWidth(25) + self.streams_add_button.setStyleSheet("QPushButton {padding:3px;padding-left:5px;border: none; text-align: left;} QPushButton:hover { " + f"background-color: rgb{str(COLOR_HIGHLIGHT)};" + f"{COLOR}" + " }") + self.streams_remove_button.setIcon(QIcon(ICON_DELETE)) + self.streams_remove_button.setMaximumWidth(25) + self.streams_remove_button.setStyleSheet("QPushButton {padding:3px;padding-left:5px;border: none; text-align: left; image-position:right} QPushButton:hover { " + f"background-color: rgb{str(COLOR_HIGHLIGHT)};" + f"{COLOR}" + " }") #+ f"{backgr_image_del}" + + self.saveLayerSelection.setStyleSheet("QPushButton {text-align: right;} QPushButton:hover { " + f"{COLOR}" + " }") + self.saveSurveyPoint.setStyleSheet("QPushButton {text-align: right;} QPushButton:hover { " + f"{COLOR}" + " }") + self.reloadButton.setStyleSheet("QPushButton {text-align: left;} QPushButton:hover { " + f"{COLOR}" + " }") + self.closeButton.setStyleSheet("QPushButton {text-align: right;} QPushButton:hover { " + f"{COLOR}" + " }") + + + self.sendModeButton.setStyleSheet("QPushButton {padding: 10px; border: 0px; " + f"color: rgb{str(SPECKLE_COLOR)};"+ "} QPushButton:hover { " + "}" ) + self.sendModeButton.setIcon(QIcon(ICON_SEND_BLUE)) + + self.receiveModeButton.setFlat(True) + self.receiveModeButton.setStyleSheet("QPushButton {padding: 10px; border: 0px;}"+ "QPushButton:hover { " + f"background-color: rgb{str(COLOR_HIGHLIGHT)};" + "}" ) + self.receiveModeButton.setIcon(QIcon(ICON_RECEIVE_BLACK)) + + self.runButton.setStyleSheet("QPushButton {color: white;border: 0px;border-radius: 17px;padding: 10px;"+ f"{BACKGR_COLOR}" + "} QPushButton:hover { "+ f"{BACKGR_COLOR_LIGHT}" + " }") + #self.runButton.setGeometry(0, 0, 150, 30) + self.runButton.setMaximumWidth(200) + self.runButton.setIcon(QIcon(ICON_SEND)) + + # insert checkbox + l = self.verticalLayout + #l_item = None + + #for i in reversed(range(self.verticalLayout.count())): + # l_item = self.verticalLayout.itemAt(i).widget() + + # add row with "experimental" checkbox + box = QWidget() + box.layout = QHBoxLayout(box) + btn = QtWidgets.QCheckBox("Send/receive in the background (experimental!)") + btn.setStyleSheet("QPushButton {color: black; border: 0px;padding: 0px;height: 40px;text-align: left;}") + box.layout.addWidget(btn) + box.layout.setContentsMargins(65, 0, 0, 0) + self.formLayout.insertRow(10,box) + self.experimental = btn + + + def runSetup(self, plugin): + + self.addDataStorage(plugin) + self.addLabel(plugin) + self.addProps(plugin) + self.createMappingDialog() + + def addProps(self, plugin): + + # add widgets that will only show on event trigger + logWidget = LogWidget(parent=self) + self.layout().addWidget(logWidget) + self.msgLog = logWidget + self.msgLog.dockwidget = self + + self.msgLog.active_account = plugin.dataStorage.active_account + self.msgLog.speckle_version = plugin.version + + # add row with "experimental" checkbox + box = QWidget() + box.layout = QHBoxLayout(box) + btn = QtWidgets.QPushButton("Apply transformations on Send") + btn.setFlat(True) + btn.setStyleSheet("QPushButton {text-align: right;} QPushButton:hover { " + f"{COLOR}" + " }") + box.layout.addWidget(btn) + box.layout.setContentsMargins(65, 0, 0, 0) + self.formLayout.insertRow(9,box) + self.setMapping = btn + + def addDataStorage(self, plugin): + self.dataStorage = plugin.dataStorage + self.dataStorage.project = plugin.qgis_project + root = self.dataStorage.project.layerTreeRoot() + self.dataStorage.all_layers = getAllLayers(root) + + def createMappingDialog(self): + root = self.dataStorage.project.layerTreeRoot() + self.dataStorage.all_layers = getAllLayers(root) + + if self.mappingSendDialog is None: + self.mappingSendDialog = MappingSendDialog(None) + self.mappingSendDialog.dataStorage = self.dataStorage + + self.mappingSendDialog.runSetup() + + def showMappingDialog(self): + # updata DataStorage + self.mappingSendDialog.show() + + def addLabel(self, plugin): + try: + exitIcon = QPixmap(ICON_LOGO) + #scaledExitIcon = exitIcon.scaled(QtCore.QSize(100, 31)) + exitActIcon = QIcon(exitIcon) + + # create a label + text_label = QtWidgets.QPushButton(" for QGIS") + text_label.setStyleSheet("border: 0px;" + "color: white;" + f"{BACKGR_COLOR}" + "top-margin: 40 px;" + "padding: 10px;" + "padding-left: 20px;" + "font-size: 15px;" + "height: 30px;" + "text-align: left;" + ) + text_label.setIcon(exitActIcon) + text_label.setIconSize(QtCore.QSize(300, 93)) + text_label.setMinimumSize(QtCore.QSize(100, 40)) + text_label.setMaximumWidth(200) + + version = "" + try: + if isinstance(plugin.version, str): version = str(plugin.version) + except: pass + + + version_label = QtWidgets.QPushButton(version) + version_label.setStyleSheet("border: 0px;" + "color: white;" + f"{BACKGR_COLOR}" + "padding-top: 15px;" + "padding-left: 0px;" + "margin-left: 0px;" + "font-size: 10px;" + "height: 30px;" + "text-align: left;" + ) + + widget = QWidget() + widget.setStyleSheet(f"{BACKGR_COLOR}") + connect_box = QHBoxLayout(widget) + connect_box.addWidget(text_label) #, alignment=Qt.AlignCenter) + connect_box.addWidget(version_label) + connect_box.setContentsMargins(0, 0, 0, 0) + self.setTitleBarWidget(widget) # for dockwidget + #self.gridLayoutTitleBar.addWidget(widget) # fro QMainWindow + + except Exception as e: + logToUser(e) + + def resizeEvent(self, event): + try: + #print("resize") + QtWidgets.QDockWidget.resizeEvent(self, event) + if self.msgLog.size().height() != 0: # visible + self.msgLog.setGeometry(0, 0, self.msgLog.parentWidget.frameSize().width(), self.msgLog.parentWidget.frameSize().height()) #.resize(self.frameSize().width(), self.frameSize().height()) + except Exception as e: + #logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + def clearDropdown(self): + try: + #self.streamIdField.clear() + self.streamBranchDropdown.clear() + self.commitDropdown.clear() + #self.layerSendModeDropdown.clear() + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + def reloadDialogUI(self, plugin): + try: + + #logToUser("long errror something something msg1", level=2, plugin= plugin) + + self.clearDropdown() + self.populateUI(plugin) + self.enableElements(plugin) + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + + def run(self, plugin): + try: + # Setup events on first load only! + self.setupOnFirstLoad(plugin) + # Connect streams section events + self.completeStreamSection(plugin) + # Populate the UI dropdowns + self.populateUI(plugin) + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + def closeEvent(self, event): + try: + self.closingPlugin.emit() + event.accept() + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + def addMsg(self, text:str, level:int, url:str, blue:bool): + #t_name = threading.current_thread().getName() + #print(t_name) + self.msgLog.addButton(text, level, url, blue) + + def setupOnFirstLoad(self, plugin): + try: + self.runButton.clicked.connect(plugin.onRunButtonClicked) + + self.msgLog.sendMessage.connect(self.addMsg) + self.setMapping.clicked.connect(self.showMappingDialog) + + self.streams_add_button.clicked.connect( plugin.onStreamAddButtonClicked ) + self.reloadButton.clicked.connect(lambda: self.refreshClicked(plugin)) + self.closeButton.clicked.connect(lambda: self.closeClicked(plugin)) + self.saveSurveyPoint.clicked.connect(plugin.set_survey_point) + self.saveLayerSelection.clicked.connect(lambda: self.populateLayerDropdown(plugin)) + self.sendModeButton.clicked.connect(lambda: self.setSendMode(plugin)) + self.layerSendModeDropdown.currentIndexChanged.connect( lambda: self.layerSendModeChange(plugin) ) + self.receiveModeButton.clicked.connect(lambda: self.setReceiveMode(plugin)) + + self.streamBranchDropdown.currentIndexChanged.connect( lambda: self.runBtnStatusChanged(plugin) ) + self.commitDropdown.currentIndexChanged.connect( lambda: self.runBtnStatusChanged(plugin) ) + + self.closingPlugin.connect(plugin.onClosePlugin) + return + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + def refreshClicked(self, plugin): + try: + try: + metrics.track("Connector Action", plugin.dataStorage.active_account, {"name": "Refresh", "connector_version": str(plugin.version)}) + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=plugin.dockwidget ) + + plugin.reloadUI() + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + def closeClicked(self, plugin): + try: + try: + metrics.track("Connector Action", plugin.dataStorage.active_account, {"name": "Close", "connector_version": str(plugin.version)}) + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=plugin.dockwidget ) + + plugin.onClosePlugin() + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + def setSendMode(self, plugin): + try: + plugin.btnAction = 0 # send + color = f"color: rgb{str(SPECKLE_COLOR)};" + self.sendModeButton.setStyleSheet("border: 0px;" + f"color: rgb{str(SPECKLE_COLOR)};" + "padding: 10px;") + self.sendModeButton.setIcon(QIcon(ICON_SEND_BLUE)) + self.sendModeButton.setFlat(False) + self.receiveModeButton.setFlat(True) + self.receiveModeButton.setStyleSheet("QPushButton {border: 0px; color: black; padding: 10px; } QPushButton:hover { " + f"background-color: rgb{str(COLOR_HIGHLIGHT)};" + " };") + self.receiveModeButton.setIcon(QIcon(ICON_RECEIVE_BLACK)) + #self.receiveModeButton.setFlat(True) + self.runButton.setProperty("text", " SEND") + self.runButton.setIcon(QIcon(ICON_SEND)) + + # enable sections only if in "saved streams" mode + if self.layerSendModeDropdown.currentIndex() == 1: self.layersWidget.setEnabled(True) + #if self.layerSendModeDropdown.currentIndex() == 1: + self.saveLayerSelection.setEnabled(True) + self.commitDropdown.setEnabled(False) + self.messageInput.setEnabled(True) + self.layerSendModeDropdown.setEnabled(True) + self.setMapping.setEnabled(True) + + self.runBtnStatusChanged(plugin) + return + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + def setReceiveMode(self, plugin): + try: + plugin.btnAction = 1 # receive + color = f"color: rgb{str(SPECKLE_COLOR)};" + self.receiveModeButton.setStyleSheet("border: 0px;" + f"color: rgb{str(SPECKLE_COLOR)};" + "padding: 10px;") + self.sendModeButton.setIcon(QIcon(ICON_SEND_BLACK)) + self.sendModeButton.setStyleSheet("QPushButton {border: 0px; color: black; padding: 10px;} QPushButton:hover { " + f"background-color: rgb{str(COLOR_HIGHLIGHT)};" + " };") + self.receiveModeButton.setIcon(QIcon(ICON_RECEIVE_BLUE)) + self.sendModeButton.setFlat(True) + self.receiveModeButton.setFlat(False) + #self.sendModeButton.setFlat(True) + self.runButton.setProperty("text", " RECEIVE") + self.runButton.setIcon(QIcon(ICON_RECEIVE)) + #self.layerSendModeChange(plugin, 1) + self.commitDropdown.setEnabled(True) + self.layersWidget.setEnabled(False) + self.messageInput.setEnabled(False) + self.saveLayerSelection.setEnabled(False) + self.layerSendModeDropdown.setEnabled(False) + self.setMapping.setEnabled(False) + + self.runBtnStatusChanged(plugin) + return + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + def completeStreamSection(self, plugin): + try: + self.streams_remove_button.clicked.connect( lambda: self.onStreamRemoveButtonClicked(plugin) ) + self.streamList.currentIndexChanged.connect( lambda: self.onActiveStreamChanged(plugin) ) + self.streamBranchDropdown.currentIndexChanged.connect( lambda: self.populateActiveCommitDropdown(plugin) ) + return + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + def populateUI(self, plugin): + try: + + self.populateLayerSendModeDropdown() + self.populateLayerDropdown(plugin, False) + #items = [self.layersWidget.item(x).text() for x in range(self.layersWidget.count())] + self.populateProjectStreams(plugin) + self.populateSurveyPoint(plugin) + + self.runBtnStatusChanged(plugin) + self.runButton.setEnabled(False) + + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + def runBtnStatusChanged(self, plugin): + try: + commitStr = str(self.commitDropdown.currentText()) + branchStr = str(self.streamBranchDropdown.currentText()) + + if plugin.btnAction == 1: # on receive + if commitStr == "": + self.runButton.setEnabled(False) + else: + self.runButton.setEnabled(True) + + if plugin.btnAction == 0: # on send + if branchStr == "": + self.runButton.setEnabled(False) + elif self.layerSendModeDropdown.currentIndex() == 1 and len(plugin.dataStorage.current_layers) == 0: # saved layers; but the list is empty + self.runButton.setEnabled(False) + else: + self.runButton.setEnabled(True) + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + + def layerSendModeChange(self, plugin, runMode = None): + try: + if self.layerSendModeDropdown.currentIndex() == 0 or runMode == 1: # by manual selection OR receive mode + #self.current_layers = [] + #self.dataStorage.current_layers = [] + self.layersWidget.setEnabled(False) + #self.saveLayerSelection.setEnabled(False) + + elif self.layerSendModeDropdown.currentIndex() == 1 and (runMode == 0 or runMode is None): # by saved AND when Send mode + self.layersWidget.setEnabled(True) + #self.saveLayerSelection.setEnabled(True) + + branchStr = str(self.streamBranchDropdown.currentText()) + if self.layerSendModeDropdown.currentIndex() == 0: + if branchStr == "": self.runButton.setEnabled(False) # by manual selection + else: self.runButton.setEnabled(True) # by manual selection + elif self.layerSendModeDropdown.currentIndex() == 1: self.runBtnStatusChanged(plugin) # by saved + + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + def populateLayerDropdown(self, plugin, bySelection: bool = True): + + try: + if not self: return + from ui.project_vars import set_project_layer_selection + + self.layersWidget.clear() + nameDisplay = [] + project = plugin.qgis_project + + if bySelection is False: # read from project data + + all_layers_ids = [l.id() for l in project.mapLayers().values()] + for layer_tuple in plugin.dataStorage.current_layers: + if layer_tuple[1].id() in all_layers_ids: + listItem = self.fillLayerList(layer_tuple[1]) + self.layersWidget.addItem(listItem) + + else: # read selected layers + # Fetch selected layers + + #plugin.current_layers = [] + self.dataStorage.current_layers.clear() + layers = getLayers(plugin, bySelection) # List[QgsLayerTreeNode] + for i, layer in enumerate(layers): + #plugin.current_layers.append((layer.name(), layer)) + self.dataStorage.current_layers.append((layer.name(), layer)) + listItem = self.fillLayerList(layer) + self.layersWidget.addItem(listItem) + + set_project_layer_selection(plugin) + + self.layersWidget.setIconSize(QtCore.QSize(20, 20)) + self.runBtnStatusChanged(plugin) + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + def fillLayerList(self, layer): + try: + icon_xxl = os.path.dirname(os.path.abspath(__file__)) + "/size-xxl.png" + listItem = QListWidgetItem(layer.name()) + + if isinstance(layer, QgsRasterLayer) and layer.width()*layer.height() > 1000000: + listItem.setIcon(QIcon(icon_xxl)) + + elif isinstance(layer, QgsVectorLayer) and layer.featureCount() > 20000: + listItem.setIcon(QIcon(icon_xxl)) + + else: + icon = QgsIconUtils().iconForLayer(layer) + listItem.setIcon(icon) + + return listItem + + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + + def populateSurveyPoint(self, plugin): + if not self: + return + try: + self.surveyPointLat.clear() + self.surveyPointLat.setText(str(plugin.lat)) + self.surveyPointLon.clear() + self.surveyPointLon.setText(str(plugin.lon)) + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + def enableElements(self, plugin): + try: + self.sendModeButton.setEnabled(plugin.is_setup) + self.receiveModeButton.setEnabled(plugin.is_setup) + self.runButton.setEnabled(plugin.is_setup) + self.streams_add_button.setEnabled(plugin.is_setup) + if plugin.is_setup is False: self.streams_remove_button.setEnabled(plugin.is_setup) + self.streamBranchDropdown.setEnabled(plugin.is_setup) + self.layerSendModeDropdown.setEnabled(plugin.is_setup) + self.commitDropdown.setEnabled(False) + self.show() + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + def populateProjectStreams(self, plugin): + try: + from ui.project_vars import set_project_streams + if not self: return + self.streamList.clear() + for stream in plugin.current_streams: + self.streamList.addItems( + [f"Stream not accessible - {stream[0].stream_id}" if stream[1] is None or isinstance(stream[1], SpeckleException) else f"{stream[1].name}, {stream[1].id} | {stream[0].stream_url.split('/streams')[0]}"] + ) + if len(plugin.current_streams)==0: self.streamList.addItems([""]) + self.streamList.addItems(["Create New Stream"]) + set_project_streams(plugin) + index = self.streamList.currentIndex() + if index == -1: self.streams_remove_button.setEnabled(False) + else: self.streams_remove_button.setEnabled(True) + + if len(plugin.current_streams)>0: plugin.active_stream = plugin.current_streams[0] + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + def onActiveStreamChanged(self, plugin): + try: + if not self: return + index = self.streamList.currentIndex() + if (len(plugin.current_streams) == 0 and index ==1) or (len(plugin.current_streams)>0 and index == len(plugin.current_streams)): + self.populateProjectStreams(plugin) + plugin.onStreamCreateClicked() + return + if len(plugin.current_streams) == 0: return + if index == -1: return + + try: plugin.active_stream = plugin.current_streams[index] + except: plugin.active_stream = None + + self.populateActiveStreamBranchDropdown(plugin) + self.populateActiveCommitDropdown(plugin) + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + def populateLayerSendModeDropdown(self): + if not self: return + try: + self.layerSendModeDropdown.clear() + self.layerSendModeDropdown.addItems( + ["Send selected layers", "Send saved layers"] + ) + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + def populateActiveStreamBranchDropdown(self, plugin): + if not self: return + try: + if plugin.active_stream is None: return + self.streamBranchDropdown.clear() + if isinstance(plugin.active_stream[1], SpeckleException): + logToUser("Some streams cannot be accessed", level = 1, plugin = self) + return + elif plugin.active_stream is None or plugin.active_stream[1] is None or plugin.active_stream[1].branches is None: + return + self.streamBranchDropdown.addItems( + [f"{branch.name}" for branch in plugin.active_stream[1].branches.items] + ) + self.streamBranchDropdown.addItems(["Create New Branch"]) + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + def populateActiveCommitDropdown(self, plugin): + if not self: return + try: + self.commitDropdown.clear() + if plugin.active_stream is None: return + branchName = self.streamBranchDropdown.currentText() + if branchName == "": return + if branchName == "Create New Branch": + self.streamBranchDropdown.setCurrentText("main") + plugin.onBranchCreateClicked() + return + branch = None + if isinstance(plugin.active_stream[1], SpeckleException): + logToUser("Some streams cannot be accessed", level = 1, plugin = self) + return + elif plugin.active_stream[1]: + for b in plugin.active_stream[1].branches.items: + if b.name == branchName: + branch = b + break + print(branch) + self.commitDropdown.addItems( + [f"{commit.id}"+ " | " + f"{commit.message}" for commit in branch.commits.items] + ) + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + print(str(e) + "::" + str(inspect.stack()[0][3])) + return + + def onStreamRemoveButtonClicked(self, plugin): + try: + from ui.project_vars import set_project_streams + if not self: return + index = self.streamList.currentIndex() + if len(plugin.current_streams) > 0: plugin.current_streams.pop(index) + plugin.active_stream = None + self.streamBranchDropdown.clear() + self.commitDropdown.clear() + #self.streamIdField.setText("") + + set_project_streams(plugin) + self.populateProjectStreams(plugin) + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + diff --git a/main_dialog_for_mainWindow.py b/main_dialog_for_mainWindow.py new file mode 100644 index 0000000..6212cc8 --- /dev/null +++ b/main_dialog_for_mainWindow.py @@ -0,0 +1,647 @@ + +import os +import sys +from typing import List +#from speckle.converter.layers import getLayers + +#import ui.speckle_qgis_dialog + +from specklepy.logging.exceptions import (SpeckleException, GraphQLException) +from PyQt5 import QtWidgets, uic +from PyQt5 import QtGui +from PyQt5.QtGui import QIcon, QPixmap +from PyQt5.QtWidgets import (QMainWindow, QApplication, QWidget, + QListWidgetItem, QAction, QDockWidget, QVBoxLayout, + QHBoxLayout, QWidget, QLabel) +from PyQt5 import QtCore +from PyQt5.QtCore import pyqtSignal, Qt, QSize, QEvent +from PyQt5 import QtGui, uic + +from specklepy.api.credentials import get_local_accounts + +import importlib + +from specklepy.api.wrapper import StreamWrapper +from specklepy.api.client import SpeckleClient +from specklepy.logging import metrics + +import arcpy + +import inspect + +try: + #from speckle.speckle_arcgis_new import Speckle + from speckle.converter.layers import getLayers, getAllProjLayers + from speckle.ui.logger import logToUser + from speckle.ui.LogWidget import LogWidget +except: + #from speckle_toolbox.esri.toolboxes.speckle.speckle_arcgis_new import Speckle + from speckle_toolbox.esri.toolboxes.speckle.converter.layers import getLayers, getAllProjLayers + from speckle_toolbox.esri.toolboxes.speckle.ui.logger import logToUser + from speckle_toolbox.esri.toolboxes.speckle.ui.LogWidget import LogWidget + +#from ui.validation import tryGetStream + +# Create module-like object +#pytPath = os.path.dirname(os.path.abspath(__file__)).replace("/speckle/ui","/Speckle.pyt") +#print(pytPath) +#pytModule = importlib.machinery.SourceFileLoader("specklePyt", pytPath ) +#specklePyt = pytModule.load_module("specklePyt") + + +# This loads your .ui file so that PyQt can populate your plugin with the elements from Qt Designer + +COLOR_HIGHLIGHT = (210,210,210) + +SPECKLE_COLOR = (59,130,246) +SPECKLE_COLOR_LIGHT = (69,140,255) +ICON_LOGO = os.path.dirname(os.path.abspath(__file__)) + "/logo-slab-white@0.5x.png" + +ICON_SEARCH = os.path.dirname(os.path.abspath(__file__)) + "/magnify.png" + +ICON_DELETE = os.path.dirname(os.path.abspath(__file__)) + "/delete.png" +ICON_DELETE_BLUE = os.path.dirname(os.path.abspath(__file__)) + "/delete-blue.png" + +ICON_SEND = os.path.dirname(os.path.abspath(__file__)) + "/cube-send.png" +ICON_RECEIVE = os.path.dirname(os.path.abspath(__file__)) + "/cube-receive.png" + +ICON_SEND_BLACK = os.path.dirname(os.path.abspath(__file__)) + "/cube-send-black.png" +ICON_RECEIVE_BLACK = os.path.dirname(os.path.abspath(__file__)) + "/cube-receive-black.png" + +ICON_SEND_BLUE = os.path.dirname(os.path.abspath(__file__)) + "/cube-send-blue.png" +ICON_RECEIVE_BLUE = os.path.dirname(os.path.abspath(__file__)) + "/cube-receive-blue.png" + +COLOR = f"color: rgb{str(SPECKLE_COLOR)};" +BACKGR_COLOR = f"background-color: rgb{str(SPECKLE_COLOR)};" +BACKGR_COLOR_LIGHT = f"background-color: rgb{str(SPECKLE_COLOR_LIGHT)};" + +ui_class = os.path.dirname(os.path.abspath(__file__)) + "/speckle_qgis_dialog_base.ui" +print(os.path.dirname(__file__)) + +class SpeckleGISDialog(QMainWindow): + + closingPlugin = pyqtSignal() + streamList: QtWidgets.QComboBox + sendModeButton: QtWidgets.QPushButton + receiveModeButton: QtWidgets.QPushButton + streamBranchDropdown: QtWidgets.QComboBox + layerSendModeDropdown: QtWidgets.QComboBox + commitDropdown: QtWidgets.QComboBox + layersWidget: QtWidgets.QListWidget + saveLayerSelection: QtWidgets.QPushButton + runButton: QtWidgets.QPushButton + msgLog: LogWidget = None + + gridLayoutTitleBar = QtWidgets.QGridLayout + + def __init__(self): + """Constructor.""" + print("START MAIN WINDOW") + super(SpeckleGISDialog, self).__init__(None)#, QtCore.Qt.WindowStaysOnTopHint) + uic.loadUi(ui_class, self) # Load the .ui file + #self.installEventFilter(self) + self.show() + #self.instances.append(1) + try: + self.streamBranchDropdown.setMaxCount(100) + self.commitDropdown.setMaxCount(100) + + self.streams_add_button.setFlat(True) + self.streams_remove_button.setFlat(True) + self.saveSurveyPoint.setFlat(True) + self.saveLayerSelection.setFlat(True) + self.reloadButton.setFlat(True) + self.closeButton.setFlat(True) + + #backgr_color = f"background-color: rgb{str(SPECKLE_COLOR)};" + #backgr_color_light = f"background-color: rgb{str(SPECKLE_COLOR_LIGHT)};" + backgr_image_del = f"border-image: url({ICON_DELETE_BLUE});" + self.streams_add_button.setIcon(QIcon(ICON_SEARCH)) + self.streams_add_button.setMaximumWidth(25) + self.streams_add_button.setStyleSheet("QPushButton {padding:3px;padding-left:5px;border: none; text-align: left;} QPushButton:hover { " + f"background-color: rgb{str(COLOR_HIGHLIGHT)};" + f"{COLOR}" + " }") + self.streams_remove_button.setIcon(QIcon(ICON_DELETE)) + self.streams_remove_button.setMaximumWidth(25) + self.streams_remove_button.setStyleSheet("QPushButton {padding:3px;padding-left:5px;border: none; text-align: left; image-position:right} QPushButton:hover { " + f"background-color: rgb{str(COLOR_HIGHLIGHT)};" + f"{COLOR}" + " }") #+ f"{backgr_image_del}" + + self.saveLayerSelection.setStyleSheet("QPushButton {text-align: right;} QPushButton:hover { " + f"{COLOR}" + " }") + self.saveSurveyPoint.setStyleSheet("QPushButton {text-align: right;} QPushButton:hover { " + f"{COLOR}" + " }") + self.reloadButton.setStyleSheet("QPushButton {text-align: left;} QPushButton:hover { " + f"{COLOR}" + " }") + self.closeButton.setStyleSheet("QPushButton {text-align: right;} QPushButton:hover { " + f"{COLOR}" + " }") + + + self.sendModeButton.setStyleSheet("QPushButton {padding: 10px; border: 0px; " + f"color: rgb{str(SPECKLE_COLOR)};"+ "} QPushButton:hover { " + "}" ) + self.sendModeButton.setIcon(QIcon(ICON_SEND_BLUE)) + + self.receiveModeButton.setFlat(True) + self.receiveModeButton.setStyleSheet("QPushButton {padding: 10px; border: 0px;}"+ "QPushButton:hover { " + f"background-color: rgb{str(COLOR_HIGHLIGHT)};" + "}" ) + self.receiveModeButton.setIcon(QIcon(ICON_RECEIVE_BLACK)) + + self.runButton.setStyleSheet("QPushButton {color: white;border: 0px;border-radius: 17px;padding: 10px;"+ f"{BACKGR_COLOR}" + "} QPushButton:hover { "+ f"{BACKGR_COLOR_LIGHT}" + " }") + self.runButton.setMaximumWidth(200) + self.runButton.setIcon(QIcon(ICON_SEND)) + + # add widgets that will only show on event trigger + logWidget = LogWidget(parent=self) + self.layout().addWidget(logWidget) + self.msgLog = logWidget + + except Exception as e: + logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + + def addProps(self, plugin): + self.msgLog.active_account = plugin.active_account + self.msgLog.speckle_version = plugin.version + + def addLabel(self, plugin): + + try: + exitIcon = QPixmap(ICON_LOGO) + exitActIcon = QIcon(exitIcon) + + # create a label + text_label = QtWidgets.QPushButton(" for ArcGIS") + text_label.setStyleSheet("border: 0px;" + "color: white;" + f"{BACKGR_COLOR}" + "top-margin: 40 px;" + "padding: 10px;" + "padding-left: 20px;" + "font-size: 15px;" + "height: 30px;" + "text-align: left;" + ) + text_label.setIcon(exitActIcon) + text_label.setIconSize(QSize(300, 93)) + text_label.setMinimumSize(QSize(100, 40)) + text_label.setMaximumWidth(220) + + version = "" + try: + if isinstance(plugin.version, str): version = str(plugin.version) + except: pass + + version_label = QtWidgets.QPushButton(f"{version}") + version_label.setStyleSheet("border: 0px;" + "color: white;" + f"{BACKGR_COLOR}" + "padding-top: 15px;" + "padding-left: 0px;" + "margin-left: 0px;" + "font-size: 10px;" + "height: 30px;" + "text-align: left;" + ) + + widget = QWidget() + widget.setStyleSheet(f"{BACKGR_COLOR}") + connect_box = QHBoxLayout(widget) + connect_box.addWidget(text_label) #, alignment=Qt.AlignCenter) + connect_box.addWidget(version_label) + connect_box.setContentsMargins(0, 0, 0, 0) + self.gridLayoutTitleBar.addWidget(widget) # fro QMainWindow + #self.setTitleBarWidget(widget) # for QDockWidget + except Exception as e: + logToUser(e) + + def resizeEvent(self, event): + try: + #print("resize") + QtWidgets.QMainWindow.resizeEvent(self, event) + if self.msgLog.size().height() != 0: # visible + self.msgLog.setGeometry(0, 0, self.msgLog.parentWidget.frameSize().width(), self.msgLog.parentWidget.frameSize().height()) #.resize(self.frameSize().width(), self.frameSize().height()) + except Exception as e: + #logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + def closeEvent(self, event): + try: + #import threading + print("Close event") + #threads = threading.enumerate() + #print(f"Threads total: {str(len(threads))}: {str(threads)}") + + #print(self.instances) + + self.closingPlugin.emit() + event.accept() + + except Exception as e: + logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + + def clearDropdown(self): + try: + #self.streamIdField.clear() + self.streamBranchDropdown.clear() + self.commitDropdown.clear() + #self.layerSendModeDropdown.clear() + except Exception as e: + logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + + def reloadDialogUI(self, plugin): + try: + self.clearDropdown() + self.populateUI(plugin) + self.enableElements(plugin) + except Exception as e: + logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + + + + def run(self, plugin): + try: + print("dockwidget run") + # Setup events on first load only! + self.setupOnFirstLoad(plugin) + # Connect streams section events + self.completeStreamSection(plugin) + # Populate the UI dropdowns + self.populateUI(plugin) + print("dockwidget run end") + except Exception as e: + logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + + + def setupOnFirstLoad(self, plugin): + try: + self.runButton.clicked.connect(plugin.onRunButtonClicked) + + self.streams_add_button.clicked.connect( plugin.onStreamAddButtonClicked ) + self.reloadButton.clicked.connect(lambda: self.refreshClicked(plugin)) + self.closeButton.clicked.connect(lambda: self.closeClicked(plugin)) + self.saveSurveyPoint.clicked.connect(plugin.set_survey_point) + self.saveLayerSelection.clicked.connect(lambda: self.populateLayerDropdown(plugin)) + self.sendModeButton.clicked.connect(lambda: self.setSendMode(plugin)) + self.layerSendModeDropdown.currentIndexChanged.connect( lambda: self.layerSendModeChange(plugin) ) + self.receiveModeButton.clicked.connect(lambda: self.setReceiveMode(plugin)) + + self.streamBranchDropdown.currentIndexChanged.connect( lambda: self.runBtnStatusChanged(plugin) ) + self.commitDropdown.currentIndexChanged.connect( lambda: self.runBtnStatusChanged(plugin) ) + + self.closingPlugin.connect(plugin.onClosePlugin) + return + + except Exception as e: + logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + + def refreshClicked(self, plugin): + try: + try: + metrics.track("Connector Action", plugin.active_account, {"name": "Refresh", "connector_version": str(plugin.version)}) + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=plugin.dockwidget ) + + plugin.reloadUI() + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + def closeClicked(self, plugin): + try: + try: + metrics.track("Connector Action", plugin.active_account, {"name": "Close", "connector_version": str(plugin.version)}) + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=plugin.dockwidget ) + + plugin.onClosePlugin() + except Exception as e: + logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + return + + def setSendMode(self, plugin): + try: + plugin.btnAction = 0 # send + color = f"color: rgb{str(SPECKLE_COLOR)};" + self.sendModeButton.setStyleSheet("border: 0px;" + f"color: rgb{str(SPECKLE_COLOR)};" + "padding: 10px;") + self.sendModeButton.setIcon(QIcon(ICON_SEND_BLUE)) + self.sendModeButton.setFlat(False) + self.receiveModeButton.setFlat(True) + self.receiveModeButton.setStyleSheet("QPushButton {border: 0px; color: black; padding: 10px; } QPushButton:hover { " + f"background-color: rgb{str(COLOR_HIGHLIGHT)};" + " };") + self.receiveModeButton.setIcon(QIcon(ICON_RECEIVE_BLACK)) + #self.receiveModeButton.setFlat(True) + self.runButton.setProperty("text", " SEND") + self.runButton.setIcon(QIcon(ICON_SEND)) + + # enable sections only if in "saved streams" mode + if self.layerSendModeDropdown.currentIndex() == 1: self.layersWidget.setEnabled(True) + if self.layerSendModeDropdown.currentIndex() == 1: self.saveLayerSelection.setEnabled(True) + self.commitDropdown.setEnabled(False) + self.messageInput.setEnabled(True) + self.layerSendModeDropdown.setEnabled(True) + + self.runBtnStatusChanged(plugin) + return + except Exception as e: + logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + + def setReceiveMode(self, plugin): + try: + plugin.btnAction = 1 # receive + color = f"color: rgb{str(SPECKLE_COLOR)};" + self.receiveModeButton.setStyleSheet("border: 0px;" + f"color: rgb{str(SPECKLE_COLOR)};" + "padding: 10px;") + self.sendModeButton.setIcon(QIcon(ICON_SEND_BLACK)) + self.sendModeButton.setStyleSheet("QPushButton {border: 0px; color: black; padding: 10px;} QPushButton:hover { " + f"background-color: rgb{str(COLOR_HIGHLIGHT)};" + " };") + self.receiveModeButton.setIcon(QIcon(ICON_RECEIVE_BLUE)) + self.sendModeButton.setFlat(True) + self.receiveModeButton.setFlat(False) + #self.sendModeButton.setFlat(True) + self.runButton.setProperty("text", " RECEIVE") + self.runButton.setIcon(QIcon(ICON_RECEIVE)) + #self.layerSendModeChange(plugin, 1) + self.commitDropdown.setEnabled(True) + self.layersWidget.setEnabled(False) + self.messageInput.setEnabled(False) + self.saveLayerSelection.setEnabled(False) + self.layerSendModeDropdown.setEnabled(False) + + self.runBtnStatusChanged(plugin) + return + except Exception as e: + logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + + def completeStreamSection(self, plugin): + self.streams_remove_button.clicked.connect( lambda: self.onStreamRemoveButtonClicked(plugin) ) + self.streamList.currentIndexChanged.connect( lambda: self.onActiveStreamChanged(plugin) ) + self.streamBranchDropdown.currentIndexChanged.connect( lambda: self.populateActiveCommitDropdown(plugin) ) + return + + def populateUI(self, plugin): + try: + self.populateLayerSendModeDropdown() + self.populateLayerDropdown(plugin, False) + #items = [self.layersWidget.item(x).text() for x in range(self.layersWidget.count())] + self.populateProjectStreams(plugin) + self.populateSurveyPoint(plugin) + + self.runBtnStatusChanged(plugin) + self.runButton.setEnabled(False) + except Exception as e: + logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + + def runBtnStatusChanged(self, plugin): + try: + commitStr = str(self.commitDropdown.currentText()) + branchStr = str(self.streamBranchDropdown.currentText()) + + if plugin.btnAction == 1: # on receive + if commitStr == "": + self.runButton.setEnabled(False) + else: + self.runButton.setEnabled(True) + + if plugin.btnAction == 0: # on send + if branchStr == "": + self.runButton.setEnabled(False) + elif branchStr != "" and self.layerSendModeDropdown.currentIndex() == 1 and len(plugin.current_layers) == 0: # saved layers; but the list is empty + self.runButton.setEnabled(False) + else: + self.runButton.setEnabled(True) + except Exception as e: + logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + + + def layerSendModeChange(self, plugin, runMode = None): + try: + print("Send mode changed") + + if self.layerSendModeDropdown.currentIndex() == 0 or runMode == 1: # by manual selection OR receive mode + self.current_layers = [] + self.layersWidget.setEnabled(False) + self.saveLayerSelection.setEnabled(False) + + elif self.layerSendModeDropdown.currentIndex() == 1 and (runMode == 0 or runMode is None): # by saved AND when Send mode + self.layersWidget.setEnabled(True) + self.saveLayerSelection.setEnabled(True) + + branchStr = str(self.streamBranchDropdown.currentText()) + if self.layerSendModeDropdown.currentIndex() == 0: + if branchStr == "": self.runButton.setEnabled(False) # by manual selection + else: self.runButton.setEnabled(True) # by manual selection + elif self.layerSendModeDropdown.currentIndex() == 1: self.runBtnStatusChanged(plugin) # by saved + except Exception as e: + logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + + + def populateLayerDropdown(self, plugin, bySelection: bool = True): + print("populate layer dropdown / clicked save selection") + if not self: return + try: + from speckle.ui.project_vars import set_project_layer_selection + except: + from speckle_toolbox.esri.toolboxes.speckle.ui.project_vars import set_project_layer_selection + + try: + self.layersWidget.clear() + nameDisplay = [] + project = plugin.gis_project + + if bySelection is False: # read from project data + print("populate layers from saved data") + #print(project) + #print(project.activeMap) + + all_layers_ids = [l.dataSource for l in getAllProjLayers(project)] + for layer_tuple in plugin.current_layers: + if layer_tuple[1].dataSource in all_layers_ids: + listItem = self.fillLayerList(layer_tuple[1]) + self.layersWidget.addItem(listItem) + + else: # read selected layers + # Fetch selected layers + print("populate layers from selection") + + plugin.current_layers = [] + layers = getLayers(plugin, bySelection) # List[QgsLayerTreeNode] + print(layers) + for i, layer in enumerate(layers): + plugin.current_layers.append((layer.name, layer)) + listItem = self.fillLayerList(layer) + self.layersWidget.addItem(listItem) + print("populate layers from selection 2") + set_project_layer_selection(plugin) + print("populate layers from selection 3") + + self.layersWidget.setIconSize(QSize(20, 20)) + self.runBtnStatusChanged(plugin) + + return + except Exception as e: + logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + + def fillLayerList(self, layer): + print("Fill layer list") + + try: + ICON_XXL = os.path.dirname(os.path.abspath(__file__)) + "/size-xxl.png" + ICON_RASTER = os.path.dirname(os.path.abspath(__file__)) + "/legend_raster.png" + ICON_POLYGON = os.path.dirname(os.path.abspath(__file__)) + "/legend_polygon.png" + ICON_LINE = os.path.dirname(os.path.abspath(__file__)) + "/legend_line.png" + ICON_POINT = os.path.dirname(os.path.abspath(__file__)) + "/legend_point.png" + + listItem = QListWidgetItem(layer.name) + #print(listItem) + + if layer.isRasterLayer: # and layer.width()*layer.height() > 1000000: + listItem.setIcon(QIcon(ICON_RASTER)) + + elif layer.isFeatureLayer: # and layer.featureCount() > 20000: + geomType = arcpy.Describe(layer.dataSource).shapeType + if geomType == "Polygon": listItem.setIcon(QIcon(ICON_POLYGON)) + elif geomType == "Polyline": listItem.setIcon(QIcon(ICON_LINE)) + elif geomType == "Point" or geomType == "Multipoint": listItem.setIcon(QIcon(ICON_POINT)) + else: + listItem.setIcon(QIcon(ICON_XXL)) + #else: + # icon = QgsIconUtils().iconForLayer(layer) + # listItem.setIcon(icon) + + return listItem + except Exception as e: + logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + + def populateSurveyPoint(self, plugin): + if not self: + return + try: + self.surveyPointLat.clear() + self.surveyPointLat.setText(str(plugin.lat)) + self.surveyPointLon.clear() + self.surveyPointLon.setText(str(plugin.lon)) + + except Exception as e: + logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + + def enableElements(self, plugin): + try: + self.sendModeButton.setEnabled(plugin.is_setup) + self.receiveModeButton.setEnabled(plugin.is_setup) + self.runButton.setEnabled(plugin.is_setup) + self.streams_add_button.setEnabled(plugin.is_setup) + if plugin.is_setup is False: self.streams_remove_button.setEnabled(plugin.is_setup) + self.streamBranchDropdown.setEnabled(plugin.is_setup) + self.layerSendModeDropdown.setEnabled(plugin.is_setup) + self.commitDropdown.setEnabled(False) + self.show() + except Exception as e: + logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + + def populateProjectStreams(self, plugin): + + try: + from speckle.ui.project_vars import set_project_streams + except: + from speckle_toolbox.esri.toolboxes.speckle.ui.project_vars import set_project_streams + + try: + if not self: return + self.streamList.clear() + for stream in plugin.current_streams: + self.streamList.addItems( + [f"Stream not accessible - {stream[0].stream_id}" if stream[1] is None or isinstance(stream[1], SpeckleException) else f"{stream[1].name}, {stream[1].id} | {stream[0].stream_url.split('/streams')[0]}"] + ) + if len(plugin.current_streams)==0: self.streamList.addItems([""]) + self.streamList.addItems(["Create New Stream"]) + set_project_streams(plugin) + index = self.streamList.currentIndex() + if index == -1: self.streams_remove_button.setEnabled(False) + else: self.streams_remove_button.setEnabled(True) + + if len(plugin.current_streams)>0: plugin.active_stream = plugin.current_streams[0] + except Exception as e: + logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + + + def onActiveStreamChanged(self, plugin): + + if not self: return + try: + index = self.streamList.currentIndex() + if (len(plugin.current_streams) == 0 and index ==1) or (len(plugin.current_streams)>0 and index == len(plugin.current_streams)): + self.populateProjectStreams(plugin) + plugin.onStreamCreateClicked() + return + if len(plugin.current_streams) == 0: return + if index == -1: return + + try: plugin.active_stream = plugin.current_streams[index] + except: plugin.active_stream = None + + self.populateActiveStreamBranchDropdown(plugin) + self.populateActiveCommitDropdown(plugin) + except Exception as e: + logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + + def populateLayerSendModeDropdown(self): + if not self: return + try: + self.layerSendModeDropdown.clear() + self.layerSendModeDropdown.addItems( + ["Send visible layers", "Send saved layers"] + ) + except Exception as e: + logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + + def populateActiveStreamBranchDropdown(self, plugin): + if not self: return + if plugin.active_stream is None: return + try: + self.streamBranchDropdown.clear() + if isinstance(plugin.active_stream[1], SpeckleException): + #logger.logToUser("Some streams cannot be accessed", Qgis.Warning) + return + elif plugin.active_stream is None or plugin.active_stream[1] is None or plugin.active_stream[1].branches is None: + return + self.streamBranchDropdown.addItems( + [f"{branch.name}" for branch in plugin.active_stream[1].branches.items] + ) + self.streamBranchDropdown.addItems(["Create New Branch"]) + except Exception as e: + logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + + def populateActiveCommitDropdown(self, plugin): + if not self: return + try: + self.commitDropdown.clear() + if plugin.active_stream is None: return + branchName = self.streamBranchDropdown.currentText() + if branchName == "": return + if branchName == "Create New Branch": + self.streamBranchDropdown.setCurrentText("main") + plugin.onBranchCreateClicked() + return + branch = None + if isinstance(plugin.active_stream[1], SpeckleException): + #logger.logToUser("Some streams cannot be accessed", Qgis.Warning) + return + elif plugin.active_stream[1]: + for b in plugin.active_stream[1].branches.items: + if b.name == branchName: + branch = b + break + try: + self.commitDropdown.addItems( + [f"{commit.id}"+ " | " + f"{commit.message}" for commit in branch.commits.items] + ) + except: pass + except Exception as e: + logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + + def onStreamRemoveButtonClicked(self, plugin): + try: + #from ui.project_vars import set_project_streams + if not self: return + index = self.streamList.currentIndex() + if len(plugin.current_streams) > 0: plugin.current_streams.pop(index) + plugin.active_stream = None + self.streamBranchDropdown.clear() + self.commitDropdown.clear() + #self.streamIdField.setText("") + + #set_project_streams(plugin) + self.populateProjectStreams(plugin) + except Exception as e: + logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + diff --git a/main_dialog_qDockwidget.ui b/main_dialog_qDockwidget.ui new file mode 100644 index 0000000..168f42f --- /dev/null +++ b/main_dialog_qDockwidget.ui @@ -0,0 +1,312 @@ + + + SpeckleQGISDialogBase + + + + 0 + 0 + 575 + 651 + + + + + + + + + + + 10 + + + 10 + + + 10 + + + 10 + + + + + + + + + + + + Stream + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 10 + 10 + + + + + + + + + + + + + Branch + + + + + + + + + + Commit + + + + + + + + + + + + + + + + + + + true + + + Send + + + + + + + Receive + + + + + + + + + + + + + + + + QAbstractItemView::NoSelection + + + + 0 + 0 + + + + QAbstractScrollArea::AdjustToContents + + + QListView::Fixed + + + QListView::ListMode + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + + + + + true + + + Save current layer selection + + + + + + + + + + Message + + + + + + + Sent XXX objects from QGIS + + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + + + + + SEND + + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + + + + + + + + Lat, Lon + + + + + + + + + 0.0 + + + + + + + 0.0 + + + + + + + + true + + + Set as a project center + + + + + + + + + + + + + + true + + + Refresh + + + + + + + true + + + Close + + + + + + + + + + + + + + + + + + + diff --git a/main_dialog_qMainWindow.ui b/main_dialog_qMainWindow.ui new file mode 100644 index 0000000..e2308bb --- /dev/null +++ b/main_dialog_qMainWindow.ui @@ -0,0 +1,316 @@ + + + SpeckleQArcGISDialog + + + + 0 + 0 + 400 + 600 + + + + + + + + + + + + + + + 20 + + + 10 + + + 30 + + + 10 + + + + + + + + + + + + Stream + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 10 + 10 + + + + + + + + + + + + + Branch + + + + + + + + + + Commit + + + + + + + + + + + + + + + + + + + true + + + Send + + + + + + + Receive + + + + + + + + + + + + + + + + QAbstractItemView::NoSelection + + + + 0 + 0 + + + + QAbstractScrollArea::AdjustToContents + + + QListView::Fixed + + + QListView::ListMode + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + + + + + true + + + Set visible layers as selection + + + + + + + + + + Message + + + + + + + Sent XXX objects from ArcGIS + + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + + + + + SEND + + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + + + + + + + + Lat, Lon + + + + + + + + + 0.0 + + + + + + + 0.0 + + + + + + + + true + + + Set as a project center + + + + + + + + + + + + + + true + + + Refresh + + + + + + + true + + + Close + + + + + + + + + + + + + + + + + + +