Browse Source

Merge branch 'main' into CURA-7647-proof-of-concept

Saumya Jain 1 year ago
parent
commit
a9a26859e7

+ 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': ['*'],

+ 72 - 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,55 @@ 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():
+                    Logger.log("w", "Could not download file from {0}".format(model_url.url()))
+                    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

+ 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
 
-######################################################################

+ 36 - 0
packaging/msi/UltiMaker-Cura.wxs.jinja

@@ -33,6 +33,21 @@
             />
         </Upgrade>
 
+        <Property Id="ASSOCIATE_URL_PROTOCOLS">
+            <RegistrySearch Id="CheckCuraProtocolHandler"
+                Type="raw"
+                Root="HKCR"
+                Key="cura"
+                Name="URL Protocol"
+            />
+            <RegistrySearch Id="CheckSlicerProtocolHandler"
+                Type="raw"
+                Root="HKCR"
+                Key="slicer"
+                Name="URL Protocol"
+            />
+        </Property>
+
         {% if "Enterprise" in app_name %}
         <Property Id="PREVIOUS_413_INSTALLED" Secure="yes" />
         <Upgrade Id="53C603BB-2B17-4206-A609-29C2E0D0B0AE">
@@ -144,11 +159,32 @@
             </Component>
         </DirectoryRef>
 
+        <!--Url Scheme-->
+        <Component Id="CuraRegistration" Guid="*" Directory="APPLICATIONFOLDER">
+            <RegistryKey Root="HKCR" Key="cura">
+                <RegistryValue Type="string" Value="URL:Cura Protocol"/>
+                <RegistryValue Type="string" Name="URL Protocol" Value=""/>
+                <RegistryValue Type="string" Key="DefaultIcon" Value="[APPLICATIONFOLDER]\{{ main_app }},1"/>
+                <RegistryValue Type="string" Key="shell\open\command" Value="&quot;[APPLICATIONFOLDER]\{{ main_app }}&quot; &quot;%1&quot;"/>
+            </RegistryKey>
+        </Component>
+
+        <Component Id="SlicerRegistration" Guid="*" Directory="APPLICATIONFOLDER">
+            <RegistryKey Root="HKCR" Key="slicer">
+                <RegistryValue Type="string" Value="URL:Slicer Protocol"/>
+                <RegistryValue Type="string" Name="URL Protocol" Value=""/>
+                <RegistryValue Type="string" Key="DefaultIcon" Value="[APPLICATIONFOLDER]\{{ main_app }},1"/>
+                <RegistryValue Type="string" Key="shell\open\command" Value="&quot;[APPLICATIONFOLDER]\{{ main_app }}&quot; &quot;%1&quot;"/>
+            </RegistryKey>
+        </Component>
+
         <Feature Id="ProductFeature" Title="{{ app_name }}" Level="1" ConfigurableDirectory="APPLICATIONFOLDER">
             <ComponentRef Id="CMP_UltiMaker_Cura_exe" />
             <ComponentRef Id="CMP_CuraEngine_exe" />
             <ComponentGroupRef Id="NewFilesGroup" />
             <ComponentRef Id="CMP_Shortcuts" />
+            <ComponentRef Id="CuraRegistration"/>
+            <ComponentRef Id="SlicerRegistration"/>
         </Feature>
         <Feature Id="UninstallOlderVersionFeature" Title="Uninstall previous versions" Level="{{ 1 if "Enterprise" in app_name else 0 }}" Description="..."/>
     </Product>