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

This commit is contained in:
Saumya Jain 2023-12-21 17:30:48 +01:00 committed by GitHub
commit a9a26859e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 135 additions and 7 deletions

View File

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

View File

@ -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,6 +331,10 @@ 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:
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)) self._files_to_open.append(os.path.abspath(filename))
def initialize(self) -> None: def initialize(self) -> None:
@ -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:
if event.file():
self._openFile(event.file()) self._openFile(event.file())
if event.url():
self._openUrl(event.url())
else: else:
if event.file():
self._open_file_queue.append(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,55 @@ 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():
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): 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

View File

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

View File

@ -33,6 +33,21 @@
/> />
</Upgrade> </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 %} {% if "Enterprise" in app_name %}
<Property Id="PREVIOUS_413_INSTALLED" Secure="yes" /> <Property Id="PREVIOUS_413_INSTALLED" Secure="yes" />
<Upgrade Id="53C603BB-2B17-4206-A609-29C2E0D0B0AE"> <Upgrade Id="53C603BB-2B17-4206-A609-29C2E0D0B0AE">
@ -144,11 +159,32 @@
</Component> </Component>
</DirectoryRef> </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"> <Feature Id="ProductFeature" Title="{{ app_name }}" Level="1" ConfigurableDirectory="APPLICATIONFOLDER">
<ComponentRef Id="CMP_UltiMaker_Cura_exe" /> <ComponentRef Id="CMP_UltiMaker_Cura_exe" />
<ComponentRef Id="CMP_CuraEngine_exe" /> <ComponentRef Id="CMP_CuraEngine_exe" />
<ComponentGroupRef Id="NewFilesGroup" /> <ComponentGroupRef Id="NewFilesGroup" />
<ComponentRef Id="CMP_Shortcuts" /> <ComponentRef Id="CMP_Shortcuts" />
<ComponentRef Id="CuraRegistration"/>
<ComponentRef Id="SlicerRegistration"/>
</Feature> </Feature>
<Feature Id="UninstallOlderVersionFeature" Title="Uninstall previous versions" Level="{{ 1 if "Enterprise" in app_name else 0 }}" Description="..."/> <Feature Id="UninstallOlderVersionFeature" Title="Uninstall previous versions" Level="{{ 1 if "Enterprise" in app_name else 0 }}" Description="..."/>
</Product> </Product>