Просмотр исходного кода

Merge branch 'main' into improve_oauth

Erwan MATHIEU 1 год назад
Родитель
Сommit
a5e158de9e

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

@@ -20,12 +20,8 @@ on:
       - 'main'
       - 'CURA-*'
       - '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:
   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_latest: ${{ needs.conan-recipe-version.outputs.recipe_id_latest }}
     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:
           - ubuntu-22.04
 
-env:
-  CONAN_ARGS: ${{ inputs.conan_args || '' }}
-  ENTERPRISE: ${{ inputs.enterprise || false }}
-  STAGING: ${{ inputs.staging || false }}
-
 jobs:
-  installer:
+  linux-installer:
     uses: ultimaker/cura-workflows/.github/workflows/cura-installer-linux.yml@main
     with:
       cura_conan_version: ${{ inputs.cura_conan_version }}
       conan_args: ${{ inputs.conan_args }}
-      enterprise: ${{ inputs.enterprise == 'true' }}
-      staging: ${{ inputs.staging == 'true' }}
+      enterprise: ${{ inputs.enterprise }}
+      staging: ${{ inputs.staging }}
       architecture: ${{ inputs.architecture }}
       operating_system: ${{ inputs.operating_system }}
     secrets: inherit

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

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

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

@@ -11,3 +11,4 @@ jobs:
     with:
       event: ${{ github.event.workflow_run.event }}
       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'
       unit_test_cmd: 'pytest --junitxml=junit_cura.xml'
       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:
           - windows-2022
 
-env:
-  CONAN_ARGS: ${{ inputs.conan_args || '' }}
-  ENTERPRISE: ${{ inputs.enterprise || false }}
-  STAGING: ${{ inputs.staging || false }}
-
 jobs:
-  installer:
+  windows-installer:
     uses: ultimaker/cura-workflows/.github/workflows/cura-installer-windows.yml@main
     with:
       cura_conan_version: ${{ inputs.cura_conan_version }}
       conan_args: ${{ inputs.conan_args }}
-      enterprise: ${{ inputs.enterprise == 'true' }}
-      staging: ${{ inputs.staging == 'true' }}
+      enterprise: ${{ inputs.enterprise }}
+      staging: ${{ inputs.staging }}
       architecture: ${{ inputs.architecture }}
       operating_system: ${{ inputs.operating_system }}
     secrets: inherit

+ 4 - 0
UltiMaker-Cura.spec.jinja

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

+ 75 - 6
cura/CuraApplication.py

@@ -2,15 +2,18 @@
 # Cura is released under the terms of the LGPLv3 or higher.
 import enum
 import os
+import re
 import sys
 import tempfile
 import time
 import platform
 from pathlib import Path
 from typing import cast, TYPE_CHECKING, Optional, Callable, List, Any, Dict
+import requests
 
 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.QtQml import qmlRegisterUncreatableType, qmlRegisterUncreatableMetaObject, qmlRegisterSingletonType, qmlRegisterType
 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._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._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._python_installs = ApplicationMetadata.PYTHON_INSTALLS
 
+        self._supported_url_schemes: List[str] = ["cura", "slicer"]
+
     @pyqtProperty(str, constant=True)
     def ultimakerCloudApiRootUrl(self) -> str:
         return UltimakerCloudConstants.CuraCloudAPIRoot
@@ -326,7 +331,11 @@ class CuraApplication(QtApplication):
             assert not "This crash is triggered by the trigger_early_crash command line argument."
 
         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:
         self.__addExpectedResourceDirsAndSearchPaths()  # Must be added before init of super
@@ -947,6 +956,8 @@ class CuraApplication(QtApplication):
             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.
             self.callLater(self._openFile, file_name)
+        for url in self._open_url_queue:
+            self.callLater(self._openUrl, url)
 
     initializationFinished = pyqtSignal()
     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 self._plugins_loaded:
-                self._openFile(event.file())
+                if event.file():
+                    self._openFile(event.file())
+                if event.url():
+                    self._openUrl(event.url())
             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.
             # Once we're at this point, everything should have been flushed already (past OnExitCallbackManager).
@@ -1542,7 +1559,7 @@ class CuraApplication(QtApplication):
         if not nodes:
             return
 
-        objects_in_filename = {}  # type: Dict[str, List[CuraSceneNode]]
+        objects_in_filename: Dict[str, List[CuraSceneNode]] = {}
         for node in nodes:
             mesh_data = node.getMeshData()
             if mesh_data:
@@ -1783,6 +1800,58 @@ class CuraApplication(QtApplication):
     def _openFile(self, 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):
         # TODO: Add the profile reader to the list of plug-ins that can be used when importing profiles.
         pass

+ 3 - 3
cura/LayerPolygon.py

@@ -67,7 +67,7 @@ class LayerPolygon:
         # Buffering the colors shouldn't be necessary as it is not
         # re-used and can save a lot of memory usage.
         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
         # or type == LayerPolygon.SkinType
@@ -75,8 +75,8 @@ class LayerPolygon:
         # 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._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:
         # 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
 ${INSTALL_TYPE}{% for files in mapped_out_paths.values() %}{% for file in files %}
 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 "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} "${UNINSTALL_PATH}"
 SectionEnd
 
-######################################################################

Некоторые файлы не были показаны из-за большого количества измененных файлов