Browse Source

Merge branch 'main' into improve_oauth

Erwan MATHIEU 1 year ago
parent
commit
a5e158de9e

+ 10 - 6
.github/workflows/conan-package.yml

@@ -20,12 +20,8 @@ on:
       - 'main'
       - 'main'
       - 'CURA-*'
       - 'CURA-*'
       - 'PP-*'
       - 'PP-*'
-      - '[0-9].[0-9]'
-      - '[0-9].[0-9][0-9]'
-    tags:
-      - '[0-9].[0-9].[0-9]*'
-      - '[0-9].[0-9].[0-9]'
-      - '[0-9].[0-9][0-9].[0-9]*'
+      - '[0-9].[0-9]*'
+      - '[0-9].[0-9][0-9]*'
 
 
 env:
 env:
   CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
   CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
@@ -44,3 +40,11 @@ jobs:
       recipe_id_full: ${{ needs.conan-recipe-version.outputs.recipe_id_full }}
       recipe_id_full: ${{ needs.conan-recipe-version.outputs.recipe_id_full }}
       recipe_id_latest: ${{ needs.conan-recipe-version.outputs.recipe_id_latest }}
       recipe_id_latest: ${{ needs.conan-recipe-version.outputs.recipe_id_latest }}
     secrets: inherit
     secrets: inherit
+
+  conan-package-create:
+    needs: [ conan-recipe-version, conan-package-export ]
+    uses: ultimaker/cura-workflows/.github/workflows/conan-package-create-linux.yml@main
+    with:
+      recipe_id_full: ${{ needs.conan-recipe-version.outputs.recipe_id_full }}
+      conan_extra_args: "-o cura:enable_i18n=True"
+    secrets: inherit

+ 3 - 8
.github/workflows/linux.yml

@@ -39,19 +39,14 @@ on:
         options:
         options:
           - ubuntu-22.04
           - ubuntu-22.04
 
 
-env:
-  CONAN_ARGS: ${{ inputs.conan_args || '' }}
-  ENTERPRISE: ${{ inputs.enterprise || false }}
-  STAGING: ${{ inputs.staging || false }}
-
 jobs:
 jobs:
-  installer:
+  linux-installer:
     uses: ultimaker/cura-workflows/.github/workflows/cura-installer-linux.yml@main
     uses: ultimaker/cura-workflows/.github/workflows/cura-installer-linux.yml@main
     with:
     with:
       cura_conan_version: ${{ inputs.cura_conan_version }}
       cura_conan_version: ${{ inputs.cura_conan_version }}
       conan_args: ${{ inputs.conan_args }}
       conan_args: ${{ inputs.conan_args }}
-      enterprise: ${{ inputs.enterprise == 'true' }}
-      staging: ${{ inputs.staging == 'true' }}
+      enterprise: ${{ inputs.enterprise }}
+      staging: ${{ inputs.staging }}
       architecture: ${{ inputs.architecture }}
       architecture: ${{ inputs.architecture }}
       operating_system: ${{ inputs.operating_system }}
       operating_system: ${{ inputs.operating_system }}
     secrets: inherit
     secrets: inherit

+ 3 - 8
.github/workflows/macos.yml

@@ -43,19 +43,14 @@ on:
           - macos-11
           - macos-11
           - macos-12
           - macos-12
 
 
-env:
-  CONAN_ARGS: ${{ inputs.conan_args || '' }}
-  ENTERPRISE: ${{ inputs.enterprise || false }}
-  STAGING: ${{ inputs.staging || false }}
-
 jobs:
 jobs:
-  installer:
+  macos-installer:
     uses: ultimaker/cura-workflows/.github/workflows/cura-installer-macos.yml@main
     uses: ultimaker/cura-workflows/.github/workflows/cura-installer-macos.yml@main
     with:
     with:
       cura_conan_version: ${{ inputs.cura_conan_version }}
       cura_conan_version: ${{ inputs.cura_conan_version }}
       conan_args: ${{ inputs.conan_args }}
       conan_args: ${{ inputs.conan_args }}
-      enterprise: ${{ inputs.enterprise == 'true' }}
-      staging: ${{ inputs.staging == 'true' }}
+      enterprise: ${{ inputs.enterprise }}
+      staging: ${{ inputs.staging }}
       architecture: ${{ inputs.architecture }}
       architecture: ${{ inputs.architecture }}
       operating_system: ${{ inputs.operating_system }}
       operating_system: ${{ inputs.operating_system }}
     secrets: inherit
     secrets: inherit

+ 1 - 0
.github/workflows/unit-test-post.yml

@@ -11,3 +11,4 @@ jobs:
     with:
     with:
       event: ${{ github.event.workflow_run.event }}
       event: ${{ github.event.workflow_run.event }}
       conclusion: ${{ github.event.workflow_run.conclusion }}
       conclusion: ${{ github.event.workflow_run.conclusion }}
+    secrets: inherit

+ 2 - 1
.github/workflows/unit-test.yml

@@ -58,4 +58,5 @@ jobs:
       conan_extra_args: '-g VirtualPythonEnv -o cura:devtools=True -c tools.build:skip_test=False'
       conan_extra_args: '-g VirtualPythonEnv -o cura:devtools=True -c tools.build:skip_test=False'
       unit_test_cmd: 'pytest --junitxml=junit_cura.xml'
       unit_test_cmd: 'pytest --junitxml=junit_cura.xml'
       unit_test_dir: 'tests'
       unit_test_dir: 'tests'
-      conan_generator_dir: './venv/bin'
+      conan_generator_dir: './venv/bin'
+    secrets: inherit

+ 3 - 8
.github/workflows/windows.yml

@@ -39,19 +39,14 @@ on:
         options:
         options:
           - windows-2022
           - windows-2022
 
 
-env:
-  CONAN_ARGS: ${{ inputs.conan_args || '' }}
-  ENTERPRISE: ${{ inputs.enterprise || false }}
-  STAGING: ${{ inputs.staging || false }}
-
 jobs:
 jobs:
-  installer:
+  windows-installer:
     uses: ultimaker/cura-workflows/.github/workflows/cura-installer-windows.yml@main
     uses: ultimaker/cura-workflows/.github/workflows/cura-installer-windows.yml@main
     with:
     with:
       cura_conan_version: ${{ inputs.cura_conan_version }}
       cura_conan_version: ${{ inputs.cura_conan_version }}
       conan_args: ${{ inputs.conan_args }}
       conan_args: ${{ inputs.conan_args }}
-      enterprise: ${{ inputs.enterprise == 'true' }}
-      staging: ${{ inputs.staging == 'true' }}
+      enterprise: ${{ inputs.enterprise }}
+      staging: ${{ inputs.staging }}
       architecture: ${{ inputs.architecture }}
       architecture: ${{ inputs.architecture }}
       operating_system: ${{ inputs.operating_system }}
       operating_system: ${{ inputs.operating_system }}
     secrets: inherit
     secrets: inherit

+ 4 - 0
UltiMaker-Cura.spec.jinja

@@ -266,6 +266,10 @@ app = UMBUNDLE(
         'CFBundlePackageType': 'APPL',
         'CFBundlePackageType': 'APPL',
         'CFBundleVersionString': {{ version }},
         'CFBundleVersionString': {{ version }},
         'CFBundleShortVersionString': {{ short_version }},
         'CFBundleShortVersionString': {{ short_version }},
+        'CFBundleURLTypes': [{
+                'CFBundleURLName': '{{ display_name }}',
+                'CFBundleURLSchemes': ['cura', 'slicer'],
+        }],
         'CFBundleDocumentTypes': [{
         'CFBundleDocumentTypes': [{
                 'CFBundleTypeRole': 'Viewer',
                 'CFBundleTypeRole': 'Viewer',
                 'CFBundleTypeExtensions': ['*'],
                 'CFBundleTypeExtensions': ['*'],

+ 75 - 6
cura/CuraApplication.py

@@ -2,15 +2,18 @@
 # Cura is released under the terms of the LGPLv3 or higher.
 # Cura is released under the terms of the LGPLv3 or higher.
 import enum
 import enum
 import os
 import os
+import re
 import sys
 import sys
 import tempfile
 import tempfile
 import time
 import time
 import platform
 import platform
 from pathlib import Path
 from pathlib import Path
 from typing import cast, TYPE_CHECKING, Optional, Callable, List, Any, Dict
 from typing import cast, TYPE_CHECKING, Optional, Callable, List, Any, Dict
+import requests
 
 
 import numpy
 import numpy
-from PyQt6.QtCore import QObject, QTimer, QUrl, pyqtSignal, pyqtProperty, QEvent, pyqtEnum, QCoreApplication
+from PyQt6.QtCore import QObject, QTimer, QUrl, QUrlQuery, pyqtSignal, pyqtProperty, QEvent, pyqtEnum, QCoreApplication, \
+    QByteArray
 from PyQt6.QtGui import QColor, QIcon
 from PyQt6.QtGui import QColor, QIcon
 from PyQt6.QtQml import qmlRegisterUncreatableType, qmlRegisterUncreatableMetaObject, qmlRegisterSingletonType, qmlRegisterType
 from PyQt6.QtQml import qmlRegisterUncreatableType, qmlRegisterUncreatableMetaObject, qmlRegisterSingletonType, qmlRegisterType
 from PyQt6.QtWidgets import QMessageBox
 from PyQt6.QtWidgets import QMessageBox
@@ -250,7 +253,7 @@ class CuraApplication(QtApplication):
         self._additional_components = {}  # Components to add to certain areas in the interface
         self._additional_components = {}  # Components to add to certain areas in the interface
 
 
         self._open_file_queue = []  # A list of files to open (after the application has started)
         self._open_file_queue = []  # A list of files to open (after the application has started)
-
+        self._open_url_queue = []  # A list of urls to open (after the application has started)
         self._update_platform_activity_timer = None
         self._update_platform_activity_timer = None
 
 
         self._sidebar_custom_menu_items = []  # type: list # Keeps list of custom menu items for the side bar
         self._sidebar_custom_menu_items = []  # type: list # Keeps list of custom menu items for the side bar
@@ -274,6 +277,8 @@ class CuraApplication(QtApplication):
         self._conan_installs = ApplicationMetadata.CONAN_INSTALLS
         self._conan_installs = ApplicationMetadata.CONAN_INSTALLS
         self._python_installs = ApplicationMetadata.PYTHON_INSTALLS
         self._python_installs = ApplicationMetadata.PYTHON_INSTALLS
 
 
+        self._supported_url_schemes: List[str] = ["cura", "slicer"]
+
     @pyqtProperty(str, constant=True)
     @pyqtProperty(str, constant=True)
     def ultimakerCloudApiRootUrl(self) -> str:
     def ultimakerCloudApiRootUrl(self) -> str:
         return UltimakerCloudConstants.CuraCloudAPIRoot
         return UltimakerCloudConstants.CuraCloudAPIRoot
@@ -326,7 +331,11 @@ class CuraApplication(QtApplication):
             assert not "This crash is triggered by the trigger_early_crash command line argument."
             assert not "This crash is triggered by the trigger_early_crash command line argument."
 
 
         for filename in self._cli_args.file:
         for filename in self._cli_args.file:
-            self._files_to_open.append(os.path.abspath(filename))
+            url = QUrl(filename)
+            if url.scheme() in self._supported_url_schemes:
+                self._open_url_queue.append(url)
+            else:
+                self._files_to_open.append(os.path.abspath(filename))
 
 
     def initialize(self) -> None:
     def initialize(self) -> None:
         self.__addExpectedResourceDirsAndSearchPaths()  # Must be added before init of super
         self.__addExpectedResourceDirsAndSearchPaths()  # Must be added before init of super
@@ -947,6 +956,8 @@ class CuraApplication(QtApplication):
             self.callLater(self._openFile, file_name)
             self.callLater(self._openFile, file_name)
         for file_name in self._open_file_queue:  # Open all the files that were queued up while plug-ins were loading.
         for file_name in self._open_file_queue:  # Open all the files that were queued up while plug-ins were loading.
             self.callLater(self._openFile, file_name)
             self.callLater(self._openFile, file_name)
+        for url in self._open_url_queue:
+            self.callLater(self._openUrl, url)
 
 
     initializationFinished = pyqtSignal()
     initializationFinished = pyqtSignal()
     showAddPrintersUncancellableDialog = pyqtSignal()  # Used to show the add printers dialog with a greyed background
     showAddPrintersUncancellableDialog = pyqtSignal()  # Used to show the add printers dialog with a greyed background
@@ -1156,9 +1167,15 @@ class CuraApplication(QtApplication):
 
 
         if event.type() == QEvent.Type.FileOpen:
         if event.type() == QEvent.Type.FileOpen:
             if self._plugins_loaded:
             if self._plugins_loaded:
-                self._openFile(event.file())
+                if event.file():
+                    self._openFile(event.file())
+                if event.url():
+                    self._openUrl(event.url())
             else:
             else:
-                self._open_file_queue.append(event.file())
+                if event.file():
+                    self._open_file_queue.append(event.file())
+                if event.url():
+                    self._open_url_queue.append(event.url())
 
 
         if int(event.type()) == 20:  # 'QEvent.Type.Quit' enum isn't there, even though it should be according to docs.
         if int(event.type()) == 20:  # 'QEvent.Type.Quit' enum isn't there, even though it should be according to docs.
             # Once we're at this point, everything should have been flushed already (past OnExitCallbackManager).
             # Once we're at this point, everything should have been flushed already (past OnExitCallbackManager).
@@ -1542,7 +1559,7 @@ class CuraApplication(QtApplication):
         if not nodes:
         if not nodes:
             return
             return
 
 
-        objects_in_filename = {}  # type: Dict[str, List[CuraSceneNode]]
+        objects_in_filename: Dict[str, List[CuraSceneNode]] = {}
         for node in nodes:
         for node in nodes:
             mesh_data = node.getMeshData()
             mesh_data = node.getMeshData()
             if mesh_data:
             if mesh_data:
@@ -1783,6 +1800,58 @@ class CuraApplication(QtApplication):
     def _openFile(self, filename):
     def _openFile(self, filename):
         self.readLocalFile(QUrl.fromLocalFile(filename))
         self.readLocalFile(QUrl.fromLocalFile(filename))
 
 
+    def _openUrl(self, url: QUrl) -> None:
+        if url.scheme() not in self._supported_url_schemes:
+            # only handle cura:// and slicer:// urls schemes
+            return
+
+        match url.host() + url.path():
+            case "open" | "open/":
+                query = QUrlQuery(url.query())
+                model_url = QUrl(query.queryItemValue("file", options=QUrl.ComponentFormattingOption.FullyDecoded))
+
+                def on_finish(response):
+                    content_disposition_header_key = QByteArray("content-disposition".encode())
+
+                    if not response.hasRawHeader(content_disposition_header_key):
+                        Logger.log("w", "Could not find Content-Disposition header in response from {0}".format(
+                            model_url.url()))
+                        # Use the last part of the url as the filename, and assume it is an STL file
+                        filename = model_url.path().split("/")[-1] + ".stl"
+                    else:
+                        # content_disposition is in the format
+                        # ```
+                        # content_disposition attachment; "filename=[FILENAME]"
+                        # ```
+                        # Use a regex to extract the filename
+                        content_disposition = str(response.rawHeader(content_disposition_header_key).data(),
+                                                  encoding='utf-8')
+                        content_disposition_match = re.match(r'attachment; filename="(?P<filename>.*)"',
+                                                             content_disposition)
+                        assert content_disposition_match is not None
+                        filename = content_disposition_match.group("filename")
+
+                    tmp = tempfile.NamedTemporaryFile(suffix=filename, delete=False)
+                    with open(tmp.name, "wb") as f:
+                        f.write(response.readAll())
+
+                    self.readLocalFile(QUrl.fromLocalFile(tmp.name), add_to_recent_files=False)
+
+                def on_error(*args, **kwargs):
+                    Logger.log("w", "Could not download file from {0}".format(model_url.url()))
+                    Message("Could not download file: " + str(model_url.url()),
+                            title= "Loading Model failed",
+                            message_type=Message.MessageType.ERROR).show()
+                    return
+
+                self.getHttpRequestManager().get(
+                    model_url.url(),
+                    callback=on_finish,
+                    error_callback=on_error,
+                )
+            case path:
+                Logger.log("w", "Unsupported url scheme path: {0}".format(path))
+
     def _addProfileReader(self, profile_reader):
     def _addProfileReader(self, profile_reader):
         # TODO: Add the profile reader to the list of plug-ins that can be used when importing profiles.
         # TODO: Add the profile reader to the list of plug-ins that can be used when importing profiles.
         pass
         pass

+ 3 - 3
cura/LayerPolygon.py

@@ -67,7 +67,7 @@ class LayerPolygon:
         # Buffering the colors shouldn't be necessary as it is not
         # Buffering the colors shouldn't be necessary as it is not
         # re-used and can save a lot of memory usage.
         # re-used and can save a lot of memory usage.
         self._color_map = LayerPolygon.getColorMap()
         self._color_map = LayerPolygon.getColorMap()
-        self._colors = self._color_map[self._types]  # type: numpy.ndarray
+        self._colors: numpy.ndarray = self._color_map[self._types]
 
 
         # When type is used as index returns true if type == LayerPolygon.InfillType
         # When type is used as index returns true if type == LayerPolygon.InfillType
         # or type == LayerPolygon.SkinType
         # or type == LayerPolygon.SkinType
@@ -75,8 +75,8 @@ class LayerPolygon:
         # Should be generated in better way, not hardcoded.
         # Should be generated in better way, not hardcoded.
         self._is_infill_or_skin_type_map = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0], dtype=bool)
         self._is_infill_or_skin_type_map = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0], dtype=bool)
 
 
-        self._build_cache_line_mesh_mask = None  # type: Optional[numpy.ndarray]
-        self._build_cache_needed_points = None  # type: Optional[numpy.ndarray]
+        self._build_cache_line_mesh_mask: Optional[numpy.ndarray] = None
+        self._build_cache_needed_points: Optional[numpy.ndarray] = None
 
 
     def buildCache(self) -> None:
     def buildCache(self) -> None:
         # For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out.
         # For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out.

+ 23 - 1
packaging/NSIS/Ultimaker-Cura.nsi.jinja

@@ -144,6 +144,23 @@ SectionEnd
 
 
 ######################################################################
 ######################################################################
 
 
+Section UrlProtocol
+
+WriteRegStr HKCR "cura" "" "URL:cura"
+WriteRegStr HKCR "cura" "URL Protocol" ""
+WriteRegStr HKCR "cura\DefaultIcon" "" "$INSTDIR\${MAIN_APP_EXE},1"
+WriteRegStr HKCR "cura\shell" "" "open"
+WriteRegStr HKCR "cura\shell\open\command" "" '"$INSTDIR\${MAIN_APP_EXE}" "%1"'
+
+WriteRegStr HKCR "slicer" "" "URL:slicer"
+WriteRegStr HKCR "slicer" "URL Protocol" ""
+WriteRegStr HKCR "slicer\DefaultIcon" "" "$INSTDIR\${MAIN_APP_EXE},1"
+WriteRegStr HKCR "slicer\shell" "" "open"
+WriteRegStr HKCR "slicer\shell\open\command" "" '"$INSTDIR\${MAIN_APP_EXE}" "%1"'
+
+SectionEnd
+######################################################################
+
 Section Uninstall
 Section Uninstall
 ${INSTALL_TYPE}{% for files in mapped_out_paths.values() %}{% for file in files %}
 ${INSTALL_TYPE}{% for files in mapped_out_paths.values() %}{% for file in files %}
 Delete "{{ file[1] }}"{% endfor %}{% endfor %}{% for rem_dir in rmdir_paths %}
 Delete "{{ file[1] }}"{% endfor %}{% endfor %}{% for rem_dir in rmdir_paths %}
@@ -187,8 +204,13 @@ RmDir "$SMPROGRAMS\{{ app_name }}"
 !insertmacro APP_UNASSOCIATE "stl" "Cura.model"
 !insertmacro APP_UNASSOCIATE "stl" "Cura.model"
 !insertmacro APP_UNASSOCIATE "3mf" "Cura.project"
 !insertmacro APP_UNASSOCIATE "3mf" "Cura.project"
 
 
+; Unassociate file associations for 'cura' protocol
+DeleteRegKey HKCR "cura"
+
+; Unassociate file associations for 'slicer' protocol
+DeleteRegKey HKCR "slicer"
+
 DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
 DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
 DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
 DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
 SectionEnd
 SectionEnd
 
 
-######################################################################

Some files were not shown because too many files changed in this diff