Merge branch 'main' into fix_postprocessing_script_folders

This commit is contained in:
fieldOfView 2023-04-15 17:47:36 +02:00
commit 378e5fedea
848 changed files with 114251 additions and 99019 deletions

View file

@ -1,6 +1,6 @@
name: Bug Report name: Bug Report
description: Create a report to help us fix issues. description: Create a report to help us fix issues.
labels: "Type: Bug" labels: ["Type: Bug", "Status: Triage"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
@ -14,7 +14,7 @@ body:
attributes: attributes:
label: Application Version label: Application Version
description: The version of Cura this issue occurs with. description: The version of Cura this issue occurs with.
placeholder: 4.9.0 placeholder: 5.0.0
validations: validations:
required: true required: true
- type: input - type: input

View file

@ -1,13 +0,0 @@
# Configuration for probot-no-response - https://github.com/probot/no-response
# Number of days of inactivity before an Issue is closed for lack of response
daysUntilClose: 14
# Label requiring a response
responseRequiredLabel: 'Status: Needs Info'
# Comment to post when closing an Issue for lack of response. Set to `false` to disable
closeComment: >
This issue has been automatically closed because there has been no response
to our request for more information from the original author. With only the
information that is currently in the issue, we don't have enough information
to take action. Please reach out if you have or find the answers we need so
that we can investigate further.

View file

@ -8,6 +8,9 @@ on:
- '4.*' - '4.*'
- 'CURA-*' - 'CURA-*'
pull_request: pull_request:
permissions:
contents: read
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest

215
.github/workflows/cura-installer.yml vendored Normal file
View file

@ -0,0 +1,215 @@
name: Cura Installer
on:
workflow_dispatch:
inputs:
cura_conan_version:
description: 'Cura Conan Version'
# Fixme: default to cura/latest@testing (which is main)
default: 'cura/latest@ultimaker/stable'
required: true
conan_config:
description: 'Conan config branch to use'
default: ''
required: false
enterprise:
description: 'Build Cura as an Enterprise edition'
required: true
default: false
type: boolean
staging:
description: 'Use staging API'
required: true
default: false
type: boolean
installer:
description: 'Create the installer'
required: true
default: false
type: boolean
# Run the nightly at 5:25 UTC on working days
schedule:
- cron: '25 3 * * 1-5'
env:
CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }}
CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }}
CONAN_LOG_RUN_TO_OUTPUT: 1
CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }}
CONAN_NON_INTERACTIVE: 1
CODESIGN_IDENTITY: ${{ secrets.CODESIGN_IDENTITY }}
MAC_NOTARIZE_USER: ${{ secrets.MAC_NOTARIZE_USER }}
MAC_NOTARIZE_PASS: ${{ secrets.MAC_NOTARIZE_PASS }}
MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}
MACOS_CERT_PASS: ${{ secrets.MACOS_CERT_PASS }}
MACOS_CERT_USER: ${{ secrets.MACOS_CERT_USER }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
MACOS_CERT_PASSPHRASE: ${{ secrets.MACOS_CERT_PASSPHRASE }}
jobs:
cura-installer-create:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ macos-10.15, windows-2022, ubuntu-20.04 ]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Python and pip
uses: actions/setup-python@v4
with:
python-version: '3.10.x'
cache: 'pip'
cache-dependency-path: .github/workflows/requirements-conan-package.txt
- name: Install Python requirements and Create default Conan profile
run: |
pip install -r .github/workflows/requirements-conan-package.txt
conan profile new default --detect
- name: Use Conan download cache (Bash)
if: ${{ runner.os != 'Windows' }}
run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache"
- name: Use Conan download cache (Powershell)
if: ${{ runner.os == 'Windows' }}
run: conan config set storage.download_cache="C:\Users\runneradmin\.conan\conan_download_cache"
- name: Cache Conan local repository packages (Bash)
uses: actions/cache@v3
if: ${{ runner.os != 'Windows' }}
with:
path: |
$HOME/.conan/data
$HOME/.conan/conan_download_cache
key: conan-${{ runner.os }}-${{ runner.arch }}
- name: Cache Conan local repository packages (Powershell)
uses: actions/cache@v3
if: ${{ runner.os == 'Windows' }}
with:
path: |
C:\Users\runneradmin\.conan\data
C:\.conan
C:\Users\runneradmin\.conan\conan_download_cache
key: conan-${{ runner.os }}-${{ runner.arch }}
- name: Install MacOS system requirements
if: ${{ runner.os == 'Macos' }}
run: brew install autoconf automake ninja
- name: Install Linux system requirements
if: ${{ runner.os == 'Linux' }}
run: |
sudo apt install build-essential checkinstall zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev -y
wget --no-check-certificate --quiet https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O $GITHUB_WORKSPACE/appimagetool
chmod +x $GITHUB_WORKSPACE/appimagetool
echo "APPIMAGETOOL_LOCATION=$GITHUB_WORKSPACE/appimagetool" >> $GITHUB_ENV
- name: Configure GPG Key Linux (Bash)
if: ${{ runner.os == 'Linux' }}
run: echo -n "$GPG_PRIVATE_KEY" | base64 --decode | gpg --import
- name: Configure Macos keychain (Bash)
if: ${{ runner.os == 'Macos' }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/um_keychain.p12
echo -n "$MACOS_CERT_P12" | base64 --decode --output $CERTIFICATE_PATH
security import $CERTIFICATE_PATH -p $MACOS_CERT_PASSPHRASE -A
security unlock -p $MACOS_CERT_USER $CERTIFICATE_PATH
- name: Clean Conan local cache
if: ${{ inputs.conan_clean_local_cache }}
run: conan remove "*" -f
- name: Get Conan configuration from branch
if: ${{ inputs.conan_config_branch != '' }}
run: conan config install https://github.com/Ultimaker/conan-config.git -a "-b ${{ inputs.conan_config_branch }}"
- name: Get Conan configuration
if: ${{ inputs.conan_config_branch == '' }}
run: conan config install https://github.com/Ultimaker/conan-config.git
- name: Create the Packages
run: conan install ${{ inputs.cura_conan_version }} --build=missing --update -c tools.env.virtualenv:powershell=True -if cura_inst -g VirtualPythonEnv -o cura:enterprise=${{ inputs.enterprise }} -o cura:staging=${{ inputs.staging }}
- name: Set Environment variables for Cura (bash)
if: ${{ runner.os != 'Windows' }}
run: |
. ./cura_inst/bin/activate_github_actions_env.sh
. ./cura_inst/bin/activate_github_actions_version_env.sh
- name: Set Environment variables for Cura (Powershell)
if: ${{ runner.os == 'Windows' }}
run: |
.\cura_inst\Scripts\activate_github_actions_env.ps1
.\cura_inst\Scripts\activate_github_actions_version_env.ps1
- name: Create the Cura dist
run: pyinstaller ./cura_inst/Ultimaker-Cura.spec
- name: Archive the artifacts (bash)
if: ${{ github.event.inputs.installer == 'false' && runner.os != 'Windows' }}
run: tar -zcf "./Ultimaker-Cura-$CURA_VERSION_FULL-${{ runner.os }}-${{ runner.arch }}.tar.gz" "./Ultimaker-Cura/"
working-directory: dist
- name: Archive the artifacts (Powershell)
if: ${{ github.event.inputs.installer == 'false' && runner.os == 'Windows' }}
run: Compress-Archive -Path ".\Ultimaker-Cura" -DestinationPath ".\Ultimaker-Cura-$Env:CURA_VERSION_FULL-${{ runner.os }}-${{ runner.arch }}.zip"
working-directory: dist
- name: Create the Windows exe installer (Powershell)
if: ${{ github.event.inputs.installer == 'true' && runner.os == 'Windows' }}
run: |
python ..\cura_inst\packaging\NSIS\nsis-configurator.py ".\Ultimaker-Cura" "..\cura_inst\packaging\NSIS\Ultimaker-Cura.nsi.jinja" "Ultimaker Cura" "Ultimaker-Cura.exe" "$Env:CURA_VERSION_MAJOR" "$Env:CURA_VERSION_MINOR" "$Env:CURA_VERSION_PATCH" "$Env:CURA_VERSION_BUILD" "Ultimaker B.V." "https://ultimaker.com" "..\cura_inst\packaging\cura_license.txt" "LZMA" "..\cura_inst\packaging\NSIS\cura_banner_nsis.bmp" "..\cura_inst\packaging\icons\Cura.ico" "Ultimaker-Cura-$Env:CURA_VERSION_FULL-${{ runner.os }}-${{ runner.arch }}.exe"
makensis /V2 /P4 Ultimaker-Cura.nsi
working-directory: dist
- name: Create the Linux AppImage (Bash)
if: ${{ github.event.inputs.installer == 'true' && runner.os == 'Linux' }}
run: python ../cura_inst/packaging/AppImage/create_appimage.py ./Ultimaker-Cura $CURA_VERSION_FULL
working-directory: dist
- name: Create the MacOS dmg (Bash) alternative
if: ${{ github.event.inputs.installer == 'true' && runner.os == 'Macos' }}
run: create-dmg --window-pos 640 360 --volicon "../cura_inst/packaging/icons/VolumeIcons_Cura.icns" --window-size 690 503 --icon-size 90 --icon "Ultimaker-Cura.app" 169 272 --app-drop-link 520 272 --eula "../cura_inst/packaging/cura_license.txt" --background "../cura_inst/packaging/icons/cura_background_dmg.png" --rez Rez "./Ultimaker-Cura.dmg" "./Ultimaker-Cura.app"
working-directory: dist
- name: Sign the MacOS dmg (Bash) alternative
if: ${{ github.event.inputs.installer == 'true' && runner.os == 'Macos' }}
run: codesign -s "$CODESIGN_IDENTITY" --timestamp -i "nl.ultimaker.cura.dmg" "./Ultimaker-Cura.dmg"
working-directory: dist
- name: Notarize the MacOS dmg (Bash) alternative
if: ${{ github.event.inputs.installer == 'true' && runner.os == 'Macos' }}
run: xcrun altool --notarize-app --primary-bundle-id "nl.ultimaker.cura" --username "$MAC_NOTARIZE_USER" --password "$MAC_NOTARIZE_PASS" --file "./Ultimaker-Cura.dmg"
working-directory: dist
- name: Create the MacOS dmg (Bash)
if: ${{ github.event.inputs.installer == 'true' && runner.os == 'Macos' }}
run: python ../cura_inst/packaging/dmg/dmg_sign_notarize.py
working-directory: dist
env:
SOURCE_DIR: ${{ env.GITHUB_WORKSPACE }}/cura_inst
DIST_DIR: ${{ env.GITHUB_WORKSPACE }}/dist
- name: Upload the artifacts
uses: actions/upload-artifact@v3
with:
name: Ultimaker-Cura-${{ env.CURA_VERSION_FULL }}-${{ runner.os }}-${{ runner.arch }}
path: |
dist/*.tar.gz
dist/*.zip
dist/*.exe
dist/*.msi
dist/*.dmg
dist/*.AppImage
dist/*.asc
retention-days: 2

31
.github/workflows/no-response.yml vendored Normal file
View file

@ -0,0 +1,31 @@
name: No Response
# Both `issue_comment` and `scheduled` event types are required for this Action
# to work properly.
on:
issue_comment:
types: [created]
schedule:
# Schedule for ten minutes after the hour, every hour
- cron: '10 * * * *'
# By specifying the access of one of the scopes, all of those that are not
# specified are set to 'none'.
permissions:
issues: write
jobs:
noResponse:
runs-on: ubuntu-latest
steps:
- uses: lee-dohm/no-response@v0.5.0
with:
token: ${{ github.token }}
daysUntilClose: 14
responseRequiredLabel: 'Status: Needs Info'
closeComment: >
This issue has been automatically closed because there has been no response
to our request for more information from the original author. With only the
information that is currently in the issue, we don't have enough information
to take action. Please reach out if you have or find the answers we need so
that we can investigate further.

View file

@ -0,0 +1,2 @@
conan
sip==6.5.1

2
.gitignore vendored
View file

@ -38,6 +38,7 @@ cura.desktop
.settings .settings
#Externally located plug-ins commonly installed by our devs. #Externally located plug-ins commonly installed by our devs.
plugins/BarbarianPlugin
plugins/cura-big-flame-graph plugins/cura-big-flame-graph
plugins/cura-camera-position plugins/cura-camera-position
plugins/cura-god-mode-plugin plugins/cura-god-mode-plugin
@ -64,6 +65,7 @@ plugins/CuraRemoteSupport
plugins/ModelCutter plugins/ModelCutter
plugins/PrintProfileCreator plugins/PrintProfileCreator
plugins/MultiPrintPlugin plugins/MultiPrintPlugin
plugins/CuraOrientationPlugin
#Build stuff #Build stuff
CMakeCache.txt CMakeCache.txt

View file

@ -1,11 +1,30 @@
# YAML 1.2 # YAML 1.2
--- ---
cff-version: 1.2.0
type: software
message: >-
If you use this software, please cite it using the
metadata from this file.
title: Ultimaker Cura
abstract: >-
A state-of-the-art slicer application built on top
of the Uranium framework.
authors: authors:
cff-version: "1.1.0" - name: Ultimaker B.V.
date-released: 2021-06-28 contact:
license: "LGPL-3.0" - email: info@ultimaker.com
message: "If you use this software, please cite it using these metadata." name: "Ultimaker B.V."
repository-code: "https://github.com/ultimaker/cura/" url: 'https://ultimaker.com/software/ultimaker-cura'
title: "Ultimaker Cura" repository-code: 'https://github.com/Ultimaker/Cura'
version: "4.12.0" license: LGPL-3.0
license-url: "https://github.com/Ultimaker/Cura/blob/main/LICENSE"
version: 5.0.0
date-released: '2022-05-17'
keywords:
- Ultimaker
- Cura
- Uranium
- Arachne
- 3DPrinting
- Slicer
... ...

View file

@ -25,9 +25,10 @@
</description> </description>
<screenshots> <screenshots>
<screenshot type="default"> <screenshot type="default">
<image>https://raw.githubusercontent.com/Ultimaker/Cura/master/screenshot.png</image> <image>https://raw.githubusercontent.com/Ultimaker/Cura/main/cura-logo.PNG</image>
</screenshot> </screenshot>
</screenshots> </screenshots>
<url type="homepage">https://ultimaker.com/software/ultimaker-cura?utm_source=cura&amp;utm_medium=software&amp;utm_campaign=cura-update-linux</url> <url type="homepage">https://ultimaker.com/software/ultimaker-cura?utm_source=cura&amp;utm_medium=software&amp;utm_campaign=cura-update-linux</url>
<translation type="gettext">Cura</translation> <translation type="gettext">Cura</translation>
<content_rating type="oars-1.1" />
</component> </component>

View file

@ -565,8 +565,8 @@ class BuildVolume(SceneNode):
self._updateScaleFactor() self._updateScaleFactor()
self._volume_aabb = AxisAlignedBox( self._volume_aabb = AxisAlignedBox(
minimum = Vector(min_w, min_h - 1.0, min_d).scale(self._scale_vector), minimum = Vector(min_w, min_h - 1.0, min_d),
maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d).scale(self._scale_vector) maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d)
) )
bed_adhesion_size = self.getEdgeDisallowedSize() bed_adhesion_size = self.getEdgeDisallowedSize()
@ -575,8 +575,8 @@ class BuildVolume(SceneNode):
# This is probably wrong in all other cases. TODO! # This is probably wrong in all other cases. TODO!
# The +1 and -1 is added as there is always a bit of extra room required to work properly. # The +1 and -1 is added as there is always a bit of extra room required to work properly.
scale_to_max_bounds = AxisAlignedBox( scale_to_max_bounds = AxisAlignedBox(
minimum = Vector(min_w + bed_adhesion_size + 1, min_h, min_d + self._disallowed_area_size - bed_adhesion_size + 1).scale(self._scale_vector), minimum = Vector(min_w + bed_adhesion_size + 1, min_h, min_d + self._disallowed_area_size - bed_adhesion_size + 1),
maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - self._disallowed_area_size + bed_adhesion_size - 1).scale(self._scale_vector) maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - self._disallowed_area_size + bed_adhesion_size - 1)
) )
self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds # type: ignore self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds # type: ignore
@ -645,7 +645,7 @@ class BuildVolume(SceneNode):
for extruder in extruders: for extruder in extruders:
extruder.propertyChanged.connect(self._onSettingPropertyChanged) extruder.propertyChanged.connect(self._onSettingPropertyChanged)
self._width = self._global_container_stack.getProperty("machine_width", "value") * self._scale_vector.x self._width = self._global_container_stack.getProperty("machine_width", "value")
machine_height = self._global_container_stack.getProperty("machine_height", "value") machine_height = self._global_container_stack.getProperty("machine_height", "value")
if self._global_container_stack.getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1: if self._global_container_stack.getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
self._height = min(self._global_container_stack.getProperty("gantry_height", "value") * self._scale_vector.z, machine_height) self._height = min(self._global_container_stack.getProperty("gantry_height", "value") * self._scale_vector.z, machine_height)
@ -656,7 +656,7 @@ class BuildVolume(SceneNode):
else: else:
self._height = self._global_container_stack.getProperty("machine_height", "value") self._height = self._global_container_stack.getProperty("machine_height", "value")
self._build_volume_message.hide() self._build_volume_message.hide()
self._depth = self._global_container_stack.getProperty("machine_depth", "value") * self._scale_vector.y self._depth = self._global_container_stack.getProperty("machine_depth", "value")
self._shape = self._global_container_stack.getProperty("machine_shape", "value") self._shape = self._global_container_stack.getProperty("machine_shape", "value")
self._updateDisallowedAreas() self._updateDisallowedAreas()
@ -752,8 +752,8 @@ class BuildVolume(SceneNode):
return return
self._updateScaleFactor() self._updateScaleFactor()
self._height = self._global_container_stack.getProperty("machine_height", "value") * self._scale_vector.z self._height = self._global_container_stack.getProperty("machine_height", "value") * self._scale_vector.z
self._width = self._global_container_stack.getProperty("machine_width", "value") * self._scale_vector.x self._width = self._global_container_stack.getProperty("machine_width", "value")
self._depth = self._global_container_stack.getProperty("machine_depth", "value") * self._scale_vector.y self._depth = self._global_container_stack.getProperty("machine_depth", "value")
self._shape = self._global_container_stack.getProperty("machine_shape", "value") self._shape = self._global_container_stack.getProperty("machine_shape", "value")
def _updateDisallowedAreasAndRebuild(self): def _updateDisallowedAreasAndRebuild(self):
@ -770,14 +770,6 @@ class BuildVolume(SceneNode):
self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks()) self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks())
self.rebuild() self.rebuild()
def _scaleAreas(self, result_areas: List[Polygon]) -> None:
if self._global_container_stack is None:
return
for i, polygon in enumerate(result_areas):
result_areas[i] = polygon.scale(
100.0 / max(100.0, self._global_container_stack.getProperty("material_shrinkage_percentage_xy", "value"))
)
def _updateDisallowedAreas(self) -> None: def _updateDisallowedAreas(self) -> None:
if not self._global_container_stack: if not self._global_container_stack:
return return
@ -833,11 +825,9 @@ class BuildVolume(SceneNode):
self._disallowed_areas = [] self._disallowed_areas = []
for extruder_id in result_areas: for extruder_id in result_areas:
self._scaleAreas(result_areas[extruder_id])
self._disallowed_areas.extend(result_areas[extruder_id]) self._disallowed_areas.extend(result_areas[extruder_id])
self._disallowed_areas_no_brim = [] self._disallowed_areas_no_brim = []
for extruder_id in result_areas_no_brim: for extruder_id in result_areas_no_brim:
self._scaleAreas(result_areas_no_brim[extruder_id])
self._disallowed_areas_no_brim.extend(result_areas_no_brim[extruder_id]) self._disallowed_areas_no_brim.extend(result_areas_no_brim[extruder_id])
def _computeDisallowedAreasPrinted(self, used_extruders): def _computeDisallowedAreasPrinted(self, used_extruders):
@ -993,6 +983,9 @@ class BuildVolume(SceneNode):
half_machine_width = self._global_container_stack.getProperty("machine_width", "value") / 2 half_machine_width = self._global_container_stack.getProperty("machine_width", "value") / 2
half_machine_depth = self._global_container_stack.getProperty("machine_depth", "value") / 2 half_machine_depth = self._global_container_stack.getProperty("machine_depth", "value") / 2
# We need at a minimum a very small border around the edge so that models can't go off the build plate
border_size = max(border_size, 0.1)
if self._shape != "elliptic": if self._shape != "elliptic":
if border_size - left_unreachable_border > 0: if border_size - left_unreachable_border > 0:
result[extruder_id].append(Polygon(numpy.array([ result[extruder_id].append(Polygon(numpy.array([

View file

@ -261,7 +261,7 @@ class CrashHandler:
opengl_instance = OpenGL.getInstance() opengl_instance = OpenGL.getInstance()
if not opengl_instance: if not opengl_instance:
self.data["opengl"] = {"version": "n/a", "vendor": "n/a", "type": "n/a"} self.data["opengl"] = {"version": "n/a", "vendor": "n/a", "type": "n/a"}
return catalog.i18nc("@label", "Not yet initialized<br/>") return catalog.i18nc("@label", "Not yet initialized") + "<br />"
info = "<ul>" info = "<ul>"
info += catalog.i18nc("@label OpenGL version", "<li>OpenGL Version: {version}</li>").format(version = opengl_instance.getOpenGLVersion()) info += catalog.i18nc("@label OpenGL version", "<li>OpenGL Version: {version}</li>").format(version = opengl_instance.getOpenGLVersion())
@ -291,6 +291,7 @@ class CrashHandler:
if with_sentry_sdk: if with_sentry_sdk:
with configure_scope() as scope: with configure_scope() as scope:
scope.set_tag("opengl_version", opengl_instance.getOpenGLVersion()) scope.set_tag("opengl_version", opengl_instance.getOpenGLVersion())
scope.set_tag("opengl_version_short", opengl_instance.getOpenGLVersionShort())
scope.set_tag("gpu_vendor", opengl_instance.getGPUVendorName()) scope.set_tag("gpu_vendor", opengl_instance.getGPUVendorName())
scope.set_tag("gpu_type", opengl_instance.getGPUType()) scope.set_tag("gpu_type", opengl_instance.getGPUType())
scope.set_tag("active_machine", active_machine_definition_id) scope.set_tag("active_machine", active_machine_definition_id)

View file

@ -115,6 +115,8 @@ from . import CuraActions
from . import PlatformPhysics from . import PlatformPhysics
from . import PrintJobPreviewImageProvider from . import PrintJobPreviewImageProvider
from .AutoSave import AutoSave from .AutoSave import AutoSave
from .Machines.Models.ActiveIntentQualitiesModel import ActiveIntentQualitiesModel
from .Machines.Models.IntentSelectionModel import IntentSelectionModel
from .SingleInstance import SingleInstance from .SingleInstance import SingleInstance
if TYPE_CHECKING: if TYPE_CHECKING:
@ -257,6 +259,7 @@ class CuraApplication(QtApplication):
from UM.CentralFileStorage import CentralFileStorage from UM.CentralFileStorage import CentralFileStorage
CentralFileStorage.setIsEnterprise(ApplicationMetadata.IsEnterpriseVersion) CentralFileStorage.setIsEnterprise(ApplicationMetadata.IsEnterpriseVersion)
Resources.setIsEnterprise(ApplicationMetadata.IsEnterpriseVersion)
@pyqtProperty(str, constant=True) @pyqtProperty(str, constant=True)
def ultimakerCloudApiRootUrl(self) -> str: def ultimakerCloudApiRootUrl(self) -> str:
@ -315,7 +318,7 @@ class CuraApplication(QtApplication):
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
super().initialize() super().initialize(ApplicationMetadata.IsEnterpriseVersion)
self._preferences.addPreference("cura/single_instance", False) self._preferences.addPreference("cura/single_instance", False)
self._use_single_instance = self._preferences.getValue("cura/single_instance") or self._cli_args.single_instance self._use_single_instance = self._preferences.getValue("cura/single_instance") or self._cli_args.single_instance
@ -348,13 +351,12 @@ class CuraApplication(QtApplication):
Resources.addExpectedDirNameInData(dir_name) Resources.addExpectedDirNameInData(dir_name)
app_root = os.path.abspath(os.path.join(os.path.dirname(sys.executable))) app_root = os.path.abspath(os.path.join(os.path.dirname(sys.executable)))
Resources.addSearchPath(os.path.join(app_root, "share", "cura", "resources")) Resources.addSecureSearchPath(os.path.join(app_root, "share", "cura", "resources"))
Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "share", "cura", "resources"))
Resources.addSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources")) Resources.addSecureSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources"))
if not hasattr(sys, "frozen"): if not hasattr(sys, "frozen"):
resource_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources") resource_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources")
Resources.addSearchPath(resource_path) Resources.addSecureSearchPath(resource_path)
@classmethod @classmethod
def _initializeSettingDefinitions(cls): def _initializeSettingDefinitions(cls):
@ -943,6 +945,7 @@ class CuraApplication(QtApplication):
self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles)) self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles))
self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing engine...")) self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing engine..."))
self.initializeEngine() self.initializeEngine()
self.getTheme().setCheckIfTrusted(ApplicationMetadata.IsEnterpriseVersion)
# Initialize UI state # Initialize UI state
controller.setActiveStage("PrepareStage") controller.setActiveStage("PrepareStage")
@ -1191,6 +1194,8 @@ class CuraApplication(QtApplication):
qmlRegisterType(NozzleModel, "Cura", 1, 0, "NozzleModel") qmlRegisterType(NozzleModel, "Cura", 1, 0, "NozzleModel")
qmlRegisterType(IntentModel, "Cura", 1, 6, "IntentModel") qmlRegisterType(IntentModel, "Cura", 1, 6, "IntentModel")
qmlRegisterType(IntentCategoryModel, "Cura", 1, 6, "IntentCategoryModel") qmlRegisterType(IntentCategoryModel, "Cura", 1, 6, "IntentCategoryModel")
qmlRegisterType(IntentSelectionModel, "Cura", 1, 7, "IntentSelectionModel")
qmlRegisterType(ActiveIntentQualitiesModel, "Cura", 1, 7, "ActiveIntentQualitiesModel")
self.processEvents() self.processEvents()
qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler") qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")

View file

@ -1,14 +1,20 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import glob
import os
from pathlib import Path
from typing import Any, cast, Dict, List, Set, Tuple, TYPE_CHECKING, Optional from typing import Any, cast, Dict, List, Set, Tuple, TYPE_CHECKING, Optional
from UM.Logger import Logger
from UM.PluginRegistry import PluginRegistry
from cura.CuraApplication import CuraApplication # To find some resource types. from cura.CuraApplication import CuraApplication # To find some resource types.
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from UM.PackageManager import PackageManager # The class we're extending. from UM.PackageManager import PackageManager # The class we're extending.
from UM.Resources import Resources # To find storage paths for some resource types. from UM.Resources import Resources # To find storage paths for some resource types.
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
if TYPE_CHECKING: if TYPE_CHECKING:
@ -51,6 +57,49 @@ class CuraPackageManager(PackageManager):
super().initialize() super().initialize()
def isMaterialBundled(self, file_name: str, guid: str):
""" Check if there is a bundled material name with file_name and guid """
for path in Resources.getSecureSearchPaths():
# Secure search paths are install directory paths, if a material is in here it must be bundled.
paths = [Path(p) for p in glob.glob(path + '/**/*.xml.fdm_material', recursive=True)]
for material in paths:
if material.name == file_name:
Logger.info(f"Found bundled material: {material.name}. Located in path: {str(material)}")
with open(material, encoding="utf-8") as f:
# Make sure the file we found has the same guid as our material
# Parsing this xml would be better but the namespace is needed to search it.
parsed_guid = PluginRegistry.getInstance().getPluginObject(
"XmlMaterialProfile").getMetadataFromSerialized(
f.read(), "GUID")
if guid == parsed_guid:
# The material we found matches both filename and GUID
return True
return False
def getMaterialFilePackageId(self, file_name: str, guid: str) -> str:
"""Get the id of the installed material package that contains file_name"""
for material_package in [f for f in os.scandir(self._installation_dirs_dict["materials"]) if f.is_dir()]:
package_id = material_package.name
for root, _, file_names in os.walk(material_package.path):
if file_name not in file_names:
# File with the name we are looking for is not in this directory
continue
with open(os.path.join(root, file_name), encoding="utf-8") as f:
# Make sure the file we found has the same guid as our material
# Parsing this xml would be better but the namespace is needed to search it.
parsed_guid = PluginRegistry.getInstance().getPluginObject("XmlMaterialProfile").getMetadataFromSerialized(
f.read(), "GUID")
if guid == parsed_guid:
return package_id
Logger.error("Could not find package_id for file: {} with GUID: {} ".format(file_name, guid))
return ""
def getMachinesUsingPackage(self, package_id: str) -> Tuple[List[Tuple[GlobalStack, str, str]], List[Tuple[GlobalStack, str, str]]]: def getMachinesUsingPackage(self, package_id: str) -> Tuple[List[Tuple[GlobalStack, str, str]], List[Tuple[GlobalStack, str, str]]]:
"""Returns a list of where the package is used """Returns a list of where the package is used

View file

@ -194,7 +194,7 @@ class MachineErrorChecker(QObject):
keys_to_recheck = {setting_key for stack, setting_key in self._stacks_and_keys_to_check} keys_to_recheck = {setting_key for stack, setting_key in self._stacks_and_keys_to_check}
keys_to_recheck.add(key) keys_to_recheck.add(key)
self._setResult(True, keys_to_recheck = keys_to_recheck) self._setResult(True, keys_to_recheck = keys_to_recheck)
continue return
# Schedule the check for the next key # Schedule the check for the next key
self._application.callLater(self._checkStack) self._application.callLater(self._checkStack)

View file

@ -0,0 +1,131 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, Set, Dict, List, Any
from PyQt6.QtCore import Qt, QObject, QTimer
import cura.CuraApplication
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
from UM.Settings.ContainerStack import ContainerStack
from cura.Machines.ContainerTree import ContainerTree
from cura.Machines.Models.MachineModelUtils import fetchLayerHeight
from cura.Machines.MaterialNode import MaterialNode
from cura.Machines.QualityGroup import QualityGroup
from cura.Settings.IntentManager import IntentManager
class ActiveIntentQualitiesModel(ListModel):
NameRole = Qt.ItemDataRole.UserRole + 1
DisplayTextRole = Qt.ItemDataRole.UserRole + 2
QualityTypeRole = Qt.ItemDataRole.UserRole + 3
LayerHeightRole = Qt.ItemDataRole.UserRole + 4
IntentCategeoryRole = Qt.ItemDataRole.UserRole + 5
def __init__(self, parent: Optional[QObject] = None) -> None:
super().__init__(parent)
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.QualityTypeRole, "quality_type")
self.addRoleName(self.LayerHeightRole, "layer_height")
self.addRoleName(self.DisplayTextRole, "display_text")
self.addRoleName(self.IntentCategeoryRole, "intent_category")
self._intent_category = ""
IntentManager.intentCategoryChangedSignal.connect(self._update)
machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager()
machine_manager.activeQualityGroupChanged.connect(self._update)
machine_manager.globalContainerChanged.connect(self._updateDelayed)
machine_manager.extruderChanged.connect(self._updateDelayed) # We also need to update if an extruder gets disabled
self._update_timer = QTimer()
self._update_timer.setInterval(100)
self._update_timer.setSingleShot(True)
self._update_timer.timeout.connect(self._update)
self._update()
def _updateDelayed(self):
self._update_timer.start()
def _onChanged(self, container: ContainerStack) -> None:
if container.getMetaDataEntry("type") == "intent":
self._updateDelayed()
def _update(self):
active_extruder_stack = cura.CuraApplication.CuraApplication.getInstance().getMachineManager().activeStack
if active_extruder_stack:
self._intent_category = active_extruder_stack.intent.getMetaDataEntry("intent_category", "")
new_items: List[Dict[str, Any]] = []
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if not global_stack:
self.setItems(new_items)
return
quality_groups = ContainerTree.getInstance().getCurrentQualityGroups()
material_nodes = self._getActiveMaterials()
added_quality_type_set: Set[str] = set()
for material_node in material_nodes:
intents = self._getIntentsForMaterial(material_node, quality_groups)
for intent in intents:
if intent["quality_type"] not in added_quality_type_set:
new_items.append(intent)
added_quality_type_set.add(intent["quality_type"])
new_items = sorted(new_items, key=lambda x: x["layer_height"])
self.setItems(new_items)
def _getActiveMaterials(self) -> Set["MaterialNode"]:
"""Get the active materials for all extruders. No duplicates will be returned"""
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
return set()
container_tree = ContainerTree.getInstance()
machine_node = container_tree.machines[global_stack.definition.getId()]
nodes: Set[MaterialNode] = set()
for extruder in global_stack.extruderList:
active_variant_name = extruder.variant.getMetaDataEntry("name")
if active_variant_name not in machine_node.variants:
Logger.log("w", "Could not find the variant %s", active_variant_name)
continue
active_variant_node = machine_node.variants[active_variant_name]
active_material_node = active_variant_node.materials.get(extruder.material.getMetaDataEntry("base_file"))
if active_material_node is None:
Logger.log("w", "Could not find the material %s", extruder.material.getMetaDataEntry("base_file"))
continue
nodes.add(active_material_node)
return nodes
def _getIntentsForMaterial(self, active_material_node: "MaterialNode", quality_groups: Dict[str, "QualityGroup"]) -> List[Dict[str, Any]]:
extruder_intents: List[Dict[str, Any]] = []
for quality_id, quality_node in active_material_node.qualities.items():
if quality_node.quality_type not in quality_groups: # Don't add the empty quality type (or anything else that would crash, defensively).
continue
quality_group = quality_groups[quality_node.quality_type]
if not quality_group.is_available:
continue
layer_height = fetchLayerHeight(quality_group)
for intent_id, intent_node in quality_node.intents.items():
if intent_node.intent_category != self._intent_category:
continue
extruder_intents.append({"name": quality_group.name,
"display_text": f"<b>{quality_group.name}</b> - {layer_height}mm",
"quality_type": quality_group.quality_type,
"layer_height": layer_height,
"intent_category": self._intent_category
})
return extruder_intents

View file

@ -135,7 +135,7 @@ class GlobalStacksModel(ListModel):
continue continue
device_name = container_stack.getMetaDataEntry("group_name", container_stack.getName()) device_name = container_stack.getMetaDataEntry("group_name", container_stack.getName())
section_name = "Connected printers" if has_remote_connection else "Preset printers" section_name = self._catalog.i18nc("@label", "Connected printers") if has_remote_connection else self._catalog.i18nc("@label", "Preset printers")
section_name = self._catalog.i18nc("@info:title", section_name) section_name = self._catalog.i18nc("@info:title", section_name)
default_removal_warning = self._catalog.i18nc( default_removal_warning = self._catalog.i18nc(

View file

@ -111,7 +111,7 @@ class IntentCategoryModel(ListModel):
except ValueError: except ValueError:
weight = 99 weight = 99
result.append({ result.append({
"name": IntentCategoryModel.translation(category, "name", category), "name": IntentCategoryModel.translation(category, "name", category.title()),
"description": IntentCategoryModel.translation(category, "description", None), "description": IntentCategoryModel.translation(category, "description", None),
"intent_category": category, "intent_category": category,
"weight": weight, "weight": weight,

View file

@ -0,0 +1,129 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import collections
from typing import OrderedDict, Optional
from PyQt6.QtCore import Qt, QTimer, QObject
import cura
from UM import i18nCatalog
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.Interfaces import ContainerInterface
from cura.Settings.IntentManager import IntentManager
catalog = i18nCatalog("cura")
class IntentSelectionModel(ListModel):
NameRole = Qt.ItemDataRole.UserRole + 1
IntentCategoryRole = Qt.ItemDataRole.UserRole + 2
WeightRole = Qt.ItemDataRole.UserRole + 3
DescriptionRole = Qt.ItemDataRole.UserRole + 4
IconRole = Qt.ItemDataRole.UserRole + 5
def __init__(self, parent: Optional[QObject] = None) -> None:
super().__init__(parent)
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.IntentCategoryRole, "intent_category")
self.addRoleName(self.WeightRole, "weight")
self.addRoleName(self.DescriptionRole, "description")
self.addRoleName(self.IconRole, "icon")
application = cura.CuraApplication.CuraApplication.getInstance()
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerChange)
ContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChange)
machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager()
machine_manager.activeMaterialChanged.connect(self._update)
machine_manager.activeVariantChanged.connect(self._update)
machine_manager.extruderChanged.connect(self._update)
extruder_manager = application.getExtruderManager()
extruder_manager.extrudersChanged.connect(self._update)
self._update_timer: QTimer = QTimer()
self._update_timer.setInterval(100)
self._update_timer.setSingleShot(True)
self._update_timer.timeout.connect(self._update)
self._onChange()
@staticmethod
def _getDefaultProfileInformation() -> OrderedDict[str, dict]:
""" Default information user-visible string. Ordered by weight. """
default_profile_information = collections.OrderedDict()
default_profile_information["default"] = {
"name": catalog.i18nc("@label", "Default"),
"icon": "GearCheck"
}
default_profile_information["visual"] = {
"name": catalog.i18nc("@label", "Visual"),
"description": catalog.i18nc("@text", "The visual profile is designed to print visual prototypes and models with the intent of high visual and surface quality."),
"icon" : "Visual"
}
default_profile_information["engineering"] = {
"name": catalog.i18nc("@label", "Engineering"),
"description": catalog.i18nc("@text", "The engineering profile is designed to print functional prototypes and end-use parts with the intent of better accuracy and for closer tolerances."),
"icon": "Nut"
}
default_profile_information["quick"] = {
"name": catalog.i18nc("@label", "Draft"),
"description": catalog.i18nc("@text", "The draft profile is designed to print initial prototypes and concept validation with the intent of significant print time reduction."),
"icon": "SpeedOMeter"
}
return default_profile_information
def _onContainerChange(self, container: ContainerInterface) -> None:
"""Updates the list of intents if an intent profile was added or removed."""
if container.getMetaDataEntry("type") == "intent":
self._update()
def _onChange(self) -> None:
self._update_timer.start()
def _update(self) -> None:
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
self.setItems([])
Logger.log("d", "No active GlobalStack, set quality profile model as empty.")
return
# Check for material compatibility
if not cura.CuraApplication.CuraApplication.getInstance().getMachineManager().activeMaterialsCompatible():
Logger.log("d", "No active material compatibility, set quality profile model as empty.")
self.setItems([])
return
default_profile_info = self._getDefaultProfileInformation()
available_categories = IntentManager.getInstance().currentAvailableIntentCategories()
result = []
for i, category in enumerate(available_categories):
profile_info = default_profile_info.get(category, {})
try:
weight = list(default_profile_info.keys()).index(category)
except ValueError:
weight = len(available_categories) + i
result.append({
"name": profile_info.get("name", category.title()),
"description": profile_info.get("description", None),
"icon" : profile_info.get("icon", ""),
"intent_category": category,
"weight": weight,
})
result.sort(key=lambda k: k["weight"])
self.setItems(result)

View file

@ -34,62 +34,6 @@ class MaterialManagementModel(QObject):
def __init__(self, parent: Optional[QObject] = None) -> None: def __init__(self, parent: Optional[QObject] = None) -> None:
super().__init__(parent = parent) super().__init__(parent = parent)
self._material_sync = CloudMaterialSync(parent=self) self._material_sync = CloudMaterialSync(parent=self)
self._checkIfNewMaterialsWereInstalled()
def _checkIfNewMaterialsWereInstalled(self) -> None:
"""
Checks whether new material packages were installed in the latest startup. If there were, then it shows
a message prompting the user to sync the materials with their printers.
"""
application = cura.CuraApplication.CuraApplication.getInstance()
for package_id, package_data in application.getPackageManager().getPackagesInstalledOnStartup().items():
if package_data["package_info"]["package_type"] == "material":
# At least one new material was installed
# TODO: This should be enabled again once CURA-8609 is merged
#self._showSyncNewMaterialsMessage()
break
def _showSyncNewMaterialsMessage(self) -> None:
sync_materials_message = Message(
text = catalog.i18nc("@action:button",
"Please sync the material profiles with your printers before starting to print."),
title = catalog.i18nc("@action:button", "New materials installed"),
message_type = Message.MessageType.WARNING,
lifetime = 0
)
sync_materials_message.addAction(
"sync",
name = catalog.i18nc("@action:button", "Sync materials"),
icon = "",
description = "Sync your newly installed materials with your printers.",
button_align = Message.ActionButtonAlignment.ALIGN_RIGHT
)
sync_materials_message.addAction(
"learn_more",
name = catalog.i18nc("@action:button", "Learn more"),
icon = "",
description = "Learn more about syncing your newly installed materials with your printers.",
button_align = Message.ActionButtonAlignment.ALIGN_LEFT,
button_style = Message.ActionButtonStyle.LINK
)
sync_materials_message.actionTriggered.connect(self._onSyncMaterialsMessageActionTriggered)
# Show the message only if there are printers that support material export
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
global_stacks = container_registry.findContainerStacks(type = "machine")
if any([stack.supportsMaterialExport for stack in global_stacks]):
sync_materials_message.show()
def _onSyncMaterialsMessageActionTriggered(self, sync_message: Message, sync_message_action: str):
if sync_message_action == "sync":
QDesktopServices.openUrl(QUrl("https://example.com/openSyncAllWindow"))
# self.openSyncAllWindow()
sync_message.hide()
elif sync_message_action == "learn_more":
QDesktopServices.openUrl(QUrl("https://support.ultimaker.com/hc/en-us/articles/360013137919?utm_source=cura&utm_medium=software&utm_campaign=sync-material-printer-message"))
@pyqtSlot("QVariant", result = bool) @pyqtSlot("QVariant", result = bool)
def canMaterialBeRemoved(self, material_node: "MaterialNode") -> bool: def canMaterialBeRemoved(self, material_node: "MaterialNode") -> bool:

View file

@ -358,8 +358,9 @@ class QualityManagementModel(ListModel):
"quality_type": quality_type, "quality_type": quality_type,
"quality_changes_group": None, "quality_changes_group": None,
"intent_category": intent_category, "intent_category": intent_category,
"section_name": catalog.i18nc("@label", intent_translations.get(intent_category, {}).get("name", catalog.i18nc("@label", "Unknown"))), "section_name": catalog.i18nc("@label", intent_translations.get(intent_category, {}).get("name", catalog.i18nc("@label", intent_category.title()))),
}) })
# Sort by quality_type for each intent category # Sort by quality_type for each intent category
intent_translations_list = list(intent_translations) intent_translations_list = list(intent_translations)

View file

@ -119,16 +119,16 @@ class PrintJobOutputModel(QObject):
@pyqtProperty(int, notify = timeTotalChanged) @pyqtProperty(int, notify = timeTotalChanged)
def timeTotal(self) -> int: def timeTotal(self) -> int:
return self._time_total return int(self._time_total)
@pyqtProperty(int, notify = timeElapsedChanged) @pyqtProperty(int, notify = timeElapsedChanged)
def timeElapsed(self) -> int: def timeElapsed(self) -> int:
return self._time_elapsed return int(self._time_elapsed)
@pyqtProperty(int, notify = timeElapsedChanged) @pyqtProperty(int, notify = timeElapsedChanged)
def timeRemaining(self) -> int: def timeRemaining(self) -> int:
# Never get a negative time remaining # Never get a negative time remaining
return max(self.timeTotal - self.timeElapsed, 0) return int(max(self.timeTotal - self.timeElapsed, 0))
@pyqtProperty(float, notify = timeElapsedChanged) @pyqtProperty(float, notify = timeElapsedChanged)
def progress(self) -> float: def progress(self) -> float:

View file

@ -114,7 +114,7 @@ class ContainerManager(QObject):
for _ in range(len(entries)): for _ in range(len(entries)):
item = item.get(entries.pop(0), {}) item = item.get(entries.pop(0), {})
if item[entry_name] != entry_value: if entry_name not in item or item[entry_name] != entry_value:
sub_item_changed = True sub_item_changed = True
item[entry_name] = entry_value item[entry_name] = entry_value
@ -206,7 +206,7 @@ class ContainerManager(QObject):
if os.path.exists(file_url): if os.path.exists(file_url):
result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"), result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_url)) catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_url))
if result == QMessageBox.ButtonRole.NoRole: if result == QMessageBox.StandardButton.No:
return {"status": "cancelled", "message": "User cancelled"} return {"status": "cancelled", "message": "User cancelled"}
try: try:

View file

@ -139,7 +139,7 @@ class CuraContainerRegistry(ContainerRegistry):
if os.path.exists(file_name): if os.path.exists(file_name):
result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"), result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name)) catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name))
if result == QMessageBox.ButtonRole.NoRole: if result == QMessageBox.StandardButton.No:
return False return False
profile_writer = self._findProfileWriter(extension, description) profile_writer = self._findProfileWriter(extension, description)

View file

@ -17,7 +17,7 @@ class CuraStackBuilder:
"""Contains helper functions to create new machines.""" """Contains helper functions to create new machines."""
@classmethod @classmethod
def createMachine(cls, name: str, definition_id: str, machine_extruder_count: Optional[int] = None) -> Optional[GlobalStack]: def createMachine(cls, name: str, definition_id: str, machine_extruder_count: Optional[int] = None, show_warning_message: bool = True) -> Optional[GlobalStack]:
"""Create a new instance of a machine. """Create a new instance of a machine.
:param name: The name of the new machine. :param name: The name of the new machine.
@ -34,6 +34,7 @@ class CuraStackBuilder:
definitions = registry.findDefinitionContainers(id = definition_id) definitions = registry.findDefinitionContainers(id = definition_id)
if not definitions: if not definitions:
if show_warning_message:
ConfigurationErrorMessage.getInstance().addFaultyContainers(definition_id) ConfigurationErrorMessage.getInstance().addFaultyContainers(definition_id)
Logger.log("w", "Definition {definition} was not found!", definition = definition_id) Logger.log("w", "Definition {definition} was not found!", definition = definition_id)
return None return None

View file

@ -1,4 +1,4 @@
# Copyright (c) 2020 Ultimaker B.V. # Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt6.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt. from PyQt6.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt.
@ -382,7 +382,10 @@ class ExtruderManager(QObject):
# "fdmextruder". We need to check a machine here so its extruder definition is correct according to this. # "fdmextruder". We need to check a machine here so its extruder definition is correct according to this.
def fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None: def fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None:
container_registry = ContainerRegistry.getInstance() container_registry = ContainerRegistry.getInstance()
expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"] expected_extruder_stack = global_stack.getMetaDataEntry("machine_extruder_trains")
if expected_extruder_stack is None:
return
expected_extruder_definition_0_id = expected_extruder_stack["0"]
try: try:
extruder_stack_0 = global_stack.extruderList[0] extruder_stack_0 = global_stack.extruderList[0]
except IndexError: except IndexError:

View file

@ -1,4 +1,4 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
@ -8,6 +8,7 @@ from UM.Logger import Logger
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
import cura.CuraApplication import cura.CuraApplication
from UM.Signal import Signal
from cura.Machines.ContainerTree import ContainerTree from cura.Machines.ContainerTree import ContainerTree
from cura.Settings.cura_empty_instance_containers import empty_intent_container from cura.Settings.cura_empty_instance_containers import empty_intent_container
@ -29,6 +30,7 @@ class IntentManager(QObject):
return cls.__instance return cls.__instance
intentCategoryChanged = pyqtSignal() #Triggered when we switch categories. intentCategoryChanged = pyqtSignal() #Triggered when we switch categories.
intentCategoryChangedSignal = Signal()
def intentMetadatas(self, definition_id: str, nozzle_name: str, material_base_file: str) -> List[Dict[str, Any]]: def intentMetadatas(self, definition_id: str, nozzle_name: str, material_base_file: str) -> List[Dict[str, Any]]:
"""Gets the metadata dictionaries of all intent profiles for a given """Gets the metadata dictionaries of all intent profiles for a given
@ -189,3 +191,4 @@ class IntentManager(QObject):
application.getMachineManager().setQualityGroupByQualityType(quality_type) application.getMachineManager().setQualityGroupByQualityType(quality_type)
if old_intent_category != intent_category: if old_intent_category != intent_category:
self.intentCategoryChanged.emit() self.intentCategoryChanged.emit()
self.intentCategoryChangedSignal.emit()

View file

@ -1611,7 +1611,7 @@ class MachineManager(QObject):
if intent_category != "default": if intent_category != "default":
intent_display_name = IntentCategoryModel.translation(intent_category, intent_display_name = IntentCategoryModel.translation(intent_category,
"name", "name",
catalog.i18nc("@label", "Unknown")) intent_category.title())
display_name = "{intent_name} - {the_rest}".format(intent_name = intent_display_name, display_name = "{intent_name} - {the_rest}".format(intent_name = intent_display_name,
the_rest = display_name) the_rest = display_name)
@ -1778,3 +1778,31 @@ class MachineManager(QObject):
abbr_machine += stripped_word abbr_machine += stripped_word
return abbr_machine return abbr_machine
@pyqtSlot(str, str, result = bool)
def intentCategoryHasQuality(self, intent_category: str, quality_type: str) -> bool:
""" Checks if there are any quality groups for active extruders that have an intent category """
quality_groups = ContainerTree.getInstance().getCurrentQualityGroups()
if quality_type in quality_groups:
quality_group = quality_groups[quality_type]
for node in quality_group.nodes_for_extruders.values():
if any(intent.intent_category == intent_category for intent in node.intents.values()):
return True
return False
@pyqtSlot(str, result = str)
def getDefaultQualityTypeForIntent(self, intent_category) -> str:
""" If there is an intent category for the default machine quality return it, otherwise return the first quality for this intent category """
machine = ContainerTree.getInstance().machines.get(self._global_container_stack.definition.getId())
if self.intentCategoryHasQuality(intent_category, machine.preferred_quality_type):
return machine.preferred_quality_type
for quality_type, quality_group in ContainerTree.getInstance().getCurrentQualityGroups().items():
for node in quality_group.nodes_for_extruders.values():
if any(intent.intent_category == intent_category for intent in node.intents.values()):
return quality_type
return ""

View file

@ -1,4 +1,4 @@
# Copyright (c) 2021 Ultimaker B.V. # Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl
@ -18,6 +18,7 @@ if TYPE_CHECKING:
from UM.Signal import Signal from UM.Signal import Signal
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
class CloudMaterialSync(QObject): class CloudMaterialSync(QObject):
""" """
Handles the synchronisation of material profiles with cloud accounts. Handles the synchronisation of material profiles with cloud accounts.
@ -44,7 +45,6 @@ class CloudMaterialSync(QObject):
break break
def openSyncAllWindow(self): def openSyncAllWindow(self):
self.reset() self.reset()
if self.sync_all_dialog is None: if self.sync_all_dialog is None:

View file

@ -18,6 +18,7 @@ import os
if sys.platform != "linux": # Turns out the Linux build _does_ use this, but we're not making an Enterprise release for that system anyway. if sys.platform != "linux": # Turns out the Linux build _does_ use this, but we're not making an Enterprise release for that system anyway.
os.environ["QT_PLUGIN_PATH"] = "" # Security workaround: Don't need it, and introduces an attack vector, so set to nul. os.environ["QT_PLUGIN_PATH"] = "" # Security workaround: Don't need it, and introduces an attack vector, so set to nul.
os.environ["QML2_IMPORT_PATH"] = "" # Security workaround: Don't need it, and introduces an attack vector, so set to nul. os.environ["QML2_IMPORT_PATH"] = "" # Security workaround: Don't need it, and introduces an attack vector, so set to nul.
os.environ["QT_OPENGL_DLL"] = "" # Security workaround: Don't need it, and introduces an attack vector, so set to nul.
from PyQt6.QtNetwork import QSslConfiguration, QSslSocket from PyQt6.QtNetwork import QSslConfiguration, QSslSocket

View file

@ -23,6 +23,7 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
from UM.Job import Job from UM.Job import Job
from UM.Preferences import Preferences from UM.Preferences import Preferences
from cura.CuraPackageManager import CuraPackageManager
from cura.Machines.ContainerTree import ContainerTree from cura.Machines.ContainerTree import ContainerTree
from cura.Settings.CuraStackBuilder import CuraStackBuilder from cura.Settings.CuraStackBuilder import CuraStackBuilder
@ -579,6 +580,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
is_printer_group = True is_printer_group = True
machine_name = group_name machine_name = group_name
# Getting missing required package ids
package_metadata = self._parse_packages_metadata(archive)
missing_package_metadata = self._filter_missing_package_metadata(package_metadata)
# Show the dialog, informing the user what is about to happen. # Show the dialog, informing the user what is about to happen.
self._dialog.setMachineConflict(machine_conflict) self._dialog.setMachineConflict(machine_conflict)
self._dialog.setIsPrinterGroup(is_printer_group) self._dialog.setIsPrinterGroup(is_printer_group)
@ -599,6 +604,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._dialog.setExtruders(extruders) self._dialog.setExtruders(extruders)
self._dialog.setVariantType(variant_type_name) self._dialog.setVariantType(variant_type_name)
self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity) self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity)
self._dialog.setMissingPackagesMetadata(missing_package_metadata)
self._dialog.show() self._dialog.show()
# Block until the dialog is closed. # Block until the dialog is closed.
@ -1243,3 +1249,29 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
metadata = data.iterfind("./um:metadata/um:name/um:label", {"um": "http://www.ultimaker.com/material"}) metadata = data.iterfind("./um:metadata/um:name/um:label", {"um": "http://www.ultimaker.com/material"})
for entry in metadata: for entry in metadata:
return entry.text return entry.text
@staticmethod
def _parse_packages_metadata(archive: zipfile.ZipFile) -> List[Dict[str, str]]:
try:
package_metadata = json.loads(archive.open("Cura/packages.json").read().decode("utf-8"))
return package_metadata["packages"]
except KeyError:
Logger.warning("No package metadata was found in .3mf file.")
except Exception:
Logger.error("Failed to load packages metadata from .3mf file.")
return []
@staticmethod
def _filter_missing_package_metadata(package_metadata: List[Dict[str, str]]) -> List[Dict[str, str]]:
"""Filters out installed packages from package_metadata"""
missing_packages = []
package_manager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager())
for package in package_metadata:
package_id = package["id"]
if not package_manager.isPackageInstalled(package_id):
missing_packages.append(package)
return missing_packages

View file

@ -1,14 +1,19 @@
# Copyright (c) 2020 Ultimaker B.V. # Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt6.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication, QUrl
from PyQt6.QtGui import QDesktopServices
from typing import List, Optional, Dict, cast from typing import List, Optional, Dict, cast
from PyQt6.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication
from UM.FlameProfiler import pyqtSlot
from UM.PluginRegistry import PluginRegistry
from UM.Application import Application
from UM.i18n import i18nCatalog
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from UM.Application import Application
from UM.FlameProfiler import pyqtSlot
from UM.i18n import i18nCatalog
from UM.Logger import Logger
from UM.Message import Message
from UM.PluginRegistry import PluginRegistry
from UM.Settings.ContainerRegistry import ContainerRegistry
from .UpdatableMachinesModel import UpdatableMachinesModel from .UpdatableMachinesModel import UpdatableMachinesModel
import os import os
@ -23,7 +28,7 @@ i18n_catalog = i18nCatalog("cura")
class WorkspaceDialog(QObject): class WorkspaceDialog(QObject):
showDialogSignal = pyqtSignal() showDialogSignal = pyqtSignal()
def __init__(self, parent = None): def __init__(self, parent = None) -> None:
super().__init__(parent) super().__init__(parent)
self._component = None self._component = None
self._context = None self._context = None
@ -59,6 +64,9 @@ class WorkspaceDialog(QObject):
self._objects_on_plate = False self._objects_on_plate = False
self._is_printer_group = False self._is_printer_group = False
self._updatable_machines_model = UpdatableMachinesModel(self) self._updatable_machines_model = UpdatableMachinesModel(self)
self._missing_package_metadata: List[Dict[str, str]] = []
self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry()
self._install_missing_package_dialog: Optional[QObject] = None
machineConflictChanged = pyqtSignal() machineConflictChanged = pyqtSignal()
qualityChangesConflictChanged = pyqtSignal() qualityChangesConflictChanged = pyqtSignal()
@ -79,6 +87,7 @@ class WorkspaceDialog(QObject):
variantTypeChanged = pyqtSignal() variantTypeChanged = pyqtSignal()
extrudersChanged = pyqtSignal() extrudersChanged = pyqtSignal()
isPrinterGroupChanged = pyqtSignal() isPrinterGroupChanged = pyqtSignal()
missingPackagesChanged = pyqtSignal()
@pyqtProperty(bool, notify = isPrinterGroupChanged) @pyqtProperty(bool, notify = isPrinterGroupChanged)
def isPrinterGroup(self) -> bool: def isPrinterGroup(self) -> bool:
@ -274,6 +283,21 @@ class WorkspaceDialog(QObject):
self._has_quality_changes_conflict = quality_changes_conflict self._has_quality_changes_conflict = quality_changes_conflict
self.qualityChangesConflictChanged.emit() self.qualityChangesConflictChanged.emit()
def setMissingPackagesMetadata(self, missing_package_metadata: List[Dict[str, str]]) -> None:
self._missing_package_metadata = missing_package_metadata
self.missingPackagesChanged.emit()
@pyqtProperty("QVariantList", notify=missingPackagesChanged)
def missingPackages(self) -> List[Dict[str, str]]:
return self._missing_package_metadata
@pyqtSlot()
def installMissingPackages(self) -> None:
marketplace_plugin = PluginRegistry.getInstance().getPluginObject("Marketplace")
if not marketplace_plugin:
Logger.warning("Could not show dialog to install missing plug-ins. Is Marketplace plug-in not available?")
marketplace_plugin.showInstallMissingPackageDialog(self._missing_package_metadata, self.showMissingMaterialsWarning) # type: ignore
def getResult(self) -> Dict[str, Optional[str]]: def getResult(self) -> Dict[str, Optional[str]]:
if "machine" in self._result and self.updatableMachinesModel.count <= 1: if "machine" in self._result and self.updatableMachinesModel.count <= 1:
self._result["machine"] = None self._result["machine"] = None
@ -360,6 +384,41 @@ class WorkspaceDialog(QObject):
time.sleep(1 / 50) time.sleep(1 / 50)
QCoreApplication.processEvents() # Ensure that the GUI does not freeze. QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
@pyqtSlot()
def showMissingMaterialsWarning(self) -> None:
result_message = Message(
i18n_catalog.i18nc("@info:status", "The material used in this project relies on some material definitions not available in Cura, this might produce undesirable print results. We highly recommend installing the full material package from the Marketplace."),
lifetime=0,
title=i18n_catalog.i18nc("@info:title", "Material profiles not installed"),
message_type=Message.MessageType.WARNING
)
result_message.addAction(
"learn_more",
name=i18n_catalog.i18nc("@action:button", "Learn more"),
icon="",
description="Learn more about project materials.",
button_align=Message.ActionButtonAlignment.ALIGN_LEFT,
button_style=Message.ActionButtonStyle.LINK
)
result_message.addAction(
"install_materials",
name=i18n_catalog.i18nc("@action:button", "Install Materials"),
icon="",
description="Install missing materials from project file.",
button_align=Message.ActionButtonAlignment.ALIGN_RIGHT,
button_style=Message.ActionButtonStyle.DEFAULT
)
result_message.actionTriggered.connect(self._onMessageActionTriggered)
result_message.show()
def _onMessageActionTriggered(self, message: Message, sync_message_action: str) -> None:
if sync_message_action == "install_materials":
self.installMissingPackages()
message.hide()
elif sync_message_action == "learn_more":
QDesktopServices.openUrl(QUrl("https://support.ultimaker.com/hc/en-us/articles/360011968360-Using-the-Ultimaker-Marketplace"))
def __show(self) -> None: def __show(self) -> None:
if self._view is None: if self._view is None:
self._createViewFromQML() self._createViewFromQML()

View file

@ -17,7 +17,7 @@ UM.Dialog
minimumWidth: UM.Theme.getSize("popup_dialog").width minimumWidth: UM.Theme.getSize("popup_dialog").width
minimumHeight: UM.Theme.getSize("popup_dialog").height minimumHeight: UM.Theme.getSize("popup_dialog").height
width: minimumWidth width: minimumWidth
margin: UM.Theme.getSize("default_margin").width
property int comboboxHeight: UM.Theme.getSize("default_margin").height property int comboboxHeight: UM.Theme.getSize("default_margin").height
onClosing: manager.notifyClosed() onClosing: manager.notifyClosed()
@ -31,10 +31,18 @@ UM.Dialog
} }
} }
Flickable
{
clip: true
width: parent.width
height: parent.height
contentHeight: dialogSummaryItem.height
ScrollBar.vertical: UM.ScrollBar { id: verticalScrollBar }
Item Item
{ {
id: dialogSummaryItem id: dialogSummaryItem
width: parent.width width: verticalScrollBar.visible ? parent.width - verticalScrollBar.width - UM.Theme.getSize("default_margin").width : parent.width
height: childrenRect.height height: childrenRect.height
anchors.margins: 10 * screenScaleFactor anchors.margins: 10 * screenScaleFactor
@ -437,24 +445,89 @@ UM.Dialog
{ {
id: warningLabel id: warningLabel
text: catalog.i18nc("@action:warning", "Loading a project will clear all models on the build plate.") text: catalog.i18nc("@action:warning", "Loading a project will clear all models on the build plate.")
wrapMode: Text.Wrap
} }
} }
} }
} }
}
property bool warning: manager.missingPackages.length > 0
footerComponent: Rectangle
{
color: warning ? UM.Theme.getColor("warning") : "transparent"
anchors.bottom: parent.bottom
width: parent.width
height: childrenRect.height + 2 * base.margin
Column
{
height: childrenRect.height
spacing: base.margin
anchors.margins: base.margin
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
RowLayout
{
id: warningRow
height: childrenRect.height
visible: warning
spacing: base.margin
UM.ColorImage
{
width: UM.Theme.getSize("extruder_icon").width
height: UM.Theme.getSize("extruder_icon").height
source: UM.Theme.getIcon("Warning")
}
UM.Label
{
id: warningText
text: "The material used in this project is currently not installed in Cura.<br/>Install the material profile and reopen the project."
}
}
Loader
{
width: parent.width
height: childrenRect.height
sourceComponent: buttonRow
}
}
}
buttonSpacing: UM.Theme.getSize("default_margin").width buttonSpacing: UM.Theme.getSize("default_margin").width
rightButtons: [ rightButtons: [
Cura.TertiaryButton Cura.TertiaryButton
{ {
visible: !warning
text: catalog.i18nc("@action:button", "Cancel") text: catalog.i18nc("@action:button", "Cancel")
onClicked: reject() onClicked: reject()
}, },
Cura.PrimaryButton Cura.PrimaryButton
{ {
visible: !warning
text: catalog.i18nc("@action:button", "Open") text: catalog.i18nc("@action:button", "Open")
onClicked: accept() onClicked: accept()
},
Cura.TertiaryButton
{
visible: warning
text: catalog.i18nc("@action:button", "Open project anyway")
onClicked: {
manager.showMissingMaterialsWarning();
accept();
}
},
Cura.PrimaryButton
{
visible: warning
text: catalog.i18nc("@action:button", "Install missing material")
onClicked: manager.installMissingPackages()
} }
] ]

View file

@ -1,15 +1,22 @@
# Copyright (c) 2015-2022 Ultimaker B.V. # Copyright (c) 2015-2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional import json
from typing import Optional, cast, List, Dict
from UM.Mesh.MeshWriter import MeshWriter from UM.Mesh.MeshWriter import MeshWriter
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from UM.Logger import Logger from UM.Logger import Logger
from UM.Math.Matrix import Matrix from UM.Math.Matrix import Matrix
from UM.Application import Application from UM.Application import Application
from UM.Message import Message
from UM.Resources import Resources
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.CuraPackageManager import CuraPackageManager
from cura.Utils.Threading import call_on_qt_thread from cura.Utils.Threading import call_on_qt_thread
from cura.Snapshot import Snapshot from cura.Snapshot import Snapshot
@ -34,6 +41,9 @@ import UM.Application
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
THUMBNAIL_PATH = "Metadata/thumbnail.png"
MODEL_PATH = "3D/3dmodel.model"
PACKAGE_METADATA_PATH = "Cura/packages.json"
class ThreeMFWriter(MeshWriter): class ThreeMFWriter(MeshWriter):
def __init__(self): def __init__(self):
@ -46,7 +56,7 @@ class ThreeMFWriter(MeshWriter):
} }
self._unit_matrix_string = self._convertMatrixToString(Matrix()) self._unit_matrix_string = self._convertMatrixToString(Matrix())
self._archive = None # type: Optional[zipfile.ZipFile] self._archive: Optional[zipfile.ZipFile] = None
self._store_archive = False self._store_archive = False
def _convertMatrixToString(self, matrix): def _convertMatrixToString(self, matrix):
@ -132,11 +142,11 @@ class ThreeMFWriter(MeshWriter):
def getArchive(self): def getArchive(self):
return self._archive return self._archive
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode): def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode) -> bool:
self._archive = None # Reset archive self._archive = None # Reset archive
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED) archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
try: try:
model_file = zipfile.ZipInfo("3D/3dmodel.model") model_file = zipfile.ZipInfo(MODEL_PATH)
# Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo. # Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo.
model_file.compress_type = zipfile.ZIP_DEFLATED model_file.compress_type = zipfile.ZIP_DEFLATED
@ -151,7 +161,7 @@ class ThreeMFWriter(MeshWriter):
relations_file = zipfile.ZipInfo("_rels/.rels") relations_file = zipfile.ZipInfo("_rels/.rels")
relations_file.compress_type = zipfile.ZIP_DEFLATED relations_file.compress_type = zipfile.ZIP_DEFLATED
relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"]) relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"])
model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel") model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/" + MODEL_PATH, Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel")
# Attempt to add a thumbnail # Attempt to add a thumbnail
snapshot = self._createSnapshot() snapshot = self._createSnapshot()
@ -160,28 +170,32 @@ class ThreeMFWriter(MeshWriter):
thumbnail_buffer.open(QBuffer.OpenModeFlag.ReadWrite) thumbnail_buffer.open(QBuffer.OpenModeFlag.ReadWrite)
snapshot.save(thumbnail_buffer, "PNG") snapshot.save(thumbnail_buffer, "PNG")
thumbnail_file = zipfile.ZipInfo("Metadata/thumbnail.png") thumbnail_file = zipfile.ZipInfo(THUMBNAIL_PATH)
# Don't try to compress snapshot file, because the PNG is pretty much as compact as it will get # Don't try to compress snapshot file, because the PNG is pretty much as compact as it will get
archive.writestr(thumbnail_file, thumbnail_buffer.data()) archive.writestr(thumbnail_file, thumbnail_buffer.data())
# Add PNG to content types file # Add PNG to content types file
thumbnail_type = ET.SubElement(content_types, "Default", Extension = "png", ContentType = "image/png") thumbnail_type = ET.SubElement(content_types, "Default", Extension = "png", ContentType = "image/png")
# Add thumbnail relation to _rels/.rels file # Add thumbnail relation to _rels/.rels file
thumbnail_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/Metadata/thumbnail.png", Id = "rel1", Type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail") thumbnail_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/" + THUMBNAIL_PATH, Id = "rel1", Type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail")
# Write material metadata
material_metadata = self._getMaterialPackageMetadata()
self._storeMetadataJson({"packages": material_metadata}, archive, PACKAGE_METADATA_PATH)
savitar_scene = Savitar.Scene() savitar_scene = Savitar.Scene()
metadata_to_store = CuraApplication.getInstance().getController().getScene().getMetaData() scene_metadata = CuraApplication.getInstance().getController().getScene().getMetaData()
for key, value in metadata_to_store.items(): for key, value in scene_metadata.items():
savitar_scene.setMetaDataEntry(key, value) savitar_scene.setMetaDataEntry(key, value)
current_time_string = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") current_time_string = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if "Application" not in metadata_to_store: if "Application" not in scene_metadata:
# This might sound a bit strange, but this field should store the original application that created # This might sound a bit strange, but this field should store the original application that created
# the 3mf. So if it was already set, leave it to whatever it was. # the 3mf. So if it was already set, leave it to whatever it was.
savitar_scene.setMetaDataEntry("Application", CuraApplication.getInstance().getApplicationDisplayName()) savitar_scene.setMetaDataEntry("Application", CuraApplication.getInstance().getApplicationDisplayName())
if "CreationDate" not in metadata_to_store: if "CreationDate" not in scene_metadata:
savitar_scene.setMetaDataEntry("CreationDate", current_time_string) savitar_scene.setMetaDataEntry("CreationDate", current_time_string)
savitar_scene.setMetaDataEntry("ModificationDate", current_time_string) savitar_scene.setMetaDataEntry("ModificationDate", current_time_string)
@ -233,6 +247,55 @@ class ThreeMFWriter(MeshWriter):
return True return True
@staticmethod
def _storeMetadataJson(metadata: Dict[str, List[Dict[str, str]]], archive: zipfile.ZipFile, path: str) -> None:
"""Stores metadata inside archive path as json file"""
metadata_file = zipfile.ZipInfo(path)
# We have to set the compress type of each file as well (it doesn't keep the type of the entire archive)
metadata_file.compress_type = zipfile.ZIP_DEFLATED
archive.writestr(metadata_file, json.dumps(metadata, separators=(", ", ": "), indent=4, skipkeys=True, ensure_ascii=False))
@staticmethod
def _getMaterialPackageMetadata() -> List[Dict[str, str]]:
"""Get metadata for installed materials in active extruder stack, this does not include bundled materials.
:return: List of material metadata dictionaries.
"""
metadata = {}
package_manager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager())
for extruder in CuraApplication.getInstance().getExtruderManager().getActiveExtruderStacks():
if not extruder.isEnabled:
# Don't export materials not in use
continue
if isinstance(extruder.material, type(ContainerRegistry.getInstance().getEmptyInstanceContainer())):
# This is an empty material container, no material to export
continue
if package_manager.isMaterialBundled(extruder.material.getFileName(), extruder.material.getMetaDataEntry("GUID")):
# Don't export bundled materials
continue
package_id = package_manager.getMaterialFilePackageId(extruder.material.getFileName(), extruder.material.getMetaDataEntry("GUID"))
package_data = package_manager.getInstalledPackageInfo(package_id)
# We failed to find the package for this material
if not package_data:
Logger.info(f"Could not find package for material in extruder {extruder.id}, skipping.")
continue
material_metadata = {"id": package_id,
"display_name": package_data.get("display_name") if package_data.get("display_name") else "",
"package_version": package_data.get("package_version") if package_data.get("package_version") else "",
"sdk_version_semver": package_data.get("sdk_version_semver") if package_data.get("sdk_version_semver") else ""}
metadata[package_id] = material_metadata
# Storing in a dict and fetching values to avoid duplicates
return list(metadata.values())
@call_on_qt_thread # must be called from the main thread because of OpenGL @call_on_qt_thread # must be called from the main thread because of OpenGL
def _createSnapshot(self): def _createSnapshot(self):
Logger.log("d", "Creating thumbnail image...") Logger.log("d", "Creating thumbnail image...")

View file

@ -5,21 +5,23 @@ import sys
from UM.Logger import Logger from UM.Logger import Logger
try: try:
from . import ThreeMFWriter from . import ThreeMFWriter
threemf_writer_was_imported = True
except ImportError: except ImportError:
Logger.log("w", "Could not import ThreeMFWriter; libSavitar may be missing") Logger.log("w", "Could not import ThreeMFWriter; libSavitar may be missing")
from . import ThreeMFWorkspaceWriter threemf_writer_was_imported = False
from . import ThreeMFWorkspaceWriter
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Platform import Platform
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
workspace_extension = "3mf" workspace_extension = "3mf"
metaData = {} metaData = {}
if "3MFWriter.ThreeMFWriter" in sys.modules: if threemf_writer_was_imported:
metaData["mesh_writer"] = { metaData["mesh_writer"] = {
"output": [{ "output": [{
"extension": "3mf", "extension": "3mf",
@ -39,6 +41,7 @@ def getMetaData():
return metaData return metaData
def register(app): def register(app):
if "3MFWriter.ThreeMFWriter" in sys.modules: if "3MFWriter.ThreeMFWriter" in sys.modules:
return {"mesh_writer": ThreeMFWriter.ThreeMFWriter(), return {"mesh_writer": ThreeMFWriter.ThreeMFWriter(),

View file

@ -5,7 +5,7 @@ import QtQuick 2.7
import QtQuick.Controls 2.1 import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import UM 1.3 as UM import UM 1.5 as UM
import Cura 1.1 as Cura import Cura 1.1 as Cura
import "../components" import "../components"
@ -22,28 +22,23 @@ Item
width: parent.width width: parent.width
anchors.fill: parent anchors.fill: parent
Label UM.Label
{ {
id: backupTitle id: backupTitle
text: catalog.i18nc("@title", "My Backups") text: catalog.i18nc("@title", "My Backups")
font: UM.Theme.getFont("large") font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")
Layout.fillWidth: true Layout.fillWidth: true
renderType: Text.NativeRendering
} }
Label UM.Label
{ {
text: catalog.i18nc("@empty_state", text: catalog.i18nc("@empty_state",
"You don't have any backups currently. Use the 'Backup Now' button to create one.") "You don't have any backups currently. Use the 'Backup Now' button to create one.")
width: parent.width width: parent.width
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
wrapMode: Label.WordWrap wrapMode: Label.WordWrap
visible: backupList.model.length == 0 visible: backupList.model.length == 0
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
renderType: Text.NativeRendering
} }
BackupList BackupList
@ -54,16 +49,13 @@ Item
Layout.fillHeight: true Layout.fillHeight: true
} }
Label UM.Label
{ {
text: catalog.i18nc("@backup_limit_info", text: catalog.i18nc("@backup_limit_info",
"During the preview phase, you'll be limited to 5 visible backups. Remove a backup to see older ones.") "During the preview phase, you'll be limited to 5 visible backups. Remove a backup to see older ones.")
width: parent.width width: parent.width
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
wrapMode: Label.WordWrap wrapMode: Label.WordWrap
visible: backupList.model.length > 4 visible: backupList.model.length > 4
renderType: Text.NativeRendering
} }
BackupListFooter BackupListFooter

View file

@ -23,7 +23,7 @@ Column
{ {
id: profileImage id: profileImage
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
source: "../images/backup.svg" source: Qt.resolvedUrl("../images/backup.svg")
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
width: Math.round(parent.width / 4) width: Math.round(parent.width / 4)
} }

View file

@ -60,7 +60,7 @@ class CuraEngineBackend(QObject, Backend):
executable_name = "CuraEngine" executable_name = "CuraEngine"
if Platform.isWindows(): if Platform.isWindows():
executable_name += ".exe" executable_name += ".exe"
default_engine_location = executable_name self._default_engine_location = executable_name
search_path = [ search_path = [
os.path.abspath(os.path.dirname(sys.executable)), os.path.abspath(os.path.dirname(sys.executable)),
@ -74,29 +74,29 @@ class CuraEngineBackend(QObject, Backend):
for path in search_path: for path in search_path:
engine_path = os.path.join(path, executable_name) engine_path = os.path.join(path, executable_name)
if os.path.isfile(engine_path): if os.path.isfile(engine_path):
default_engine_location = engine_path self._default_engine_location = engine_path
break break
if Platform.isLinux() and not default_engine_location: if Platform.isLinux() and not self._default_engine_location:
if not os.getenv("PATH"): if not os.getenv("PATH"):
raise OSError("There is something wrong with your Linux installation.") raise OSError("There is something wrong with your Linux installation.")
for pathdir in cast(str, os.getenv("PATH")).split(os.pathsep): for pathdir in cast(str, os.getenv("PATH")).split(os.pathsep):
execpath = os.path.join(pathdir, executable_name) execpath = os.path.join(pathdir, executable_name)
if os.path.exists(execpath): if os.path.exists(execpath):
default_engine_location = execpath self._default_engine_location = execpath
break break
application = CuraApplication.getInstance() #type: CuraApplication application = CuraApplication.getInstance() #type: CuraApplication
self._multi_build_plate_model = None #type: Optional[MultiBuildPlateModel] self._multi_build_plate_model = None #type: Optional[MultiBuildPlateModel]
self._machine_error_checker = None #type: Optional[MachineErrorChecker] self._machine_error_checker = None #type: Optional[MachineErrorChecker]
if not default_engine_location: if not self._default_engine_location:
raise EnvironmentError("Could not find CuraEngine") raise EnvironmentError("Could not find CuraEngine")
Logger.log("i", "Found CuraEngine at: %s", default_engine_location) Logger.log("i", "Found CuraEngine at: %s", self._default_engine_location)
default_engine_location = os.path.abspath(default_engine_location) self._default_engine_location = os.path.abspath(self._default_engine_location)
application.getPreferences().addPreference("backend/location", default_engine_location) application.getPreferences().addPreference("backend/location", self._default_engine_location)
# Workaround to disable layer view processing if layer view is not active. # Workaround to disable layer view processing if layer view is not active.
self._layer_view_active = False #type: bool self._layer_view_active = False #type: bool
@ -215,7 +215,12 @@ class CuraEngineBackend(QObject, Backend):
This is useful for debugging and used to actually start the engine. This is useful for debugging and used to actually start the engine.
:return: list of commands and args / parameters. :return: list of commands and args / parameters.
""" """
command = [CuraApplication.getInstance().getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), ""] from cura import ApplicationMetadata
if ApplicationMetadata.IsEnterpriseVersion:
command = [self._default_engine_location]
else:
command = [CuraApplication.getInstance().getPreferences().getValue("backend/location")]
command += ["connect", "127.0.0.1:{0}".format(self._port), ""]
parser = argparse.ArgumentParser(prog = "cura", add_help = False) parser = argparse.ArgumentParser(prog = "cura", add_help = False)
parser.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.") parser.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.")

View file

@ -369,6 +369,9 @@ class StartSliceJob(Job):
result["material_name"] = stack.material.getMetaDataEntry("name", "") result["material_name"] = stack.material.getMetaDataEntry("name", "")
result["material_brand"] = stack.material.getMetaDataEntry("brand", "") result["material_brand"] = stack.material.getMetaDataEntry("brand", "")
result["quality_name"] = stack.quality.getMetaDataEntry("name", "")
result["quality_changes_name"] = stack.qualityChanges.getMetaDataEntry("name")
# Renamed settings. # Renamed settings.
result["print_bed_temperature"] = result["material_bed_temperature"] result["print_bed_temperature"] = result["material_bed_temperature"]
result["print_temperature"] = result["material_print_temperature"] result["print_temperature"] = result["material_print_temperature"]

View file

@ -5,7 +5,7 @@ import QtQuick 2.15
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import UM 1.2 as UM import UM 1.5 as UM
import Cura 1.6 as Cura import Cura 1.6 as Cura
import DigitalFactory 1.0 as DF import DigitalFactory 1.0 as DF
@ -64,12 +64,10 @@ Popup
} }
} }
Label UM.Label
{ {
id: projectNameLabel id: projectNameLabel
text: "Project Name" text: "Project Name"
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
anchors anchors
{ {
top: createNewLibraryProjectLabel.bottom top: createNewLibraryProjectLabel.bottom
@ -107,13 +105,12 @@ Popup
} }
} }
Label UM.Label
{ {
id: errorWhileCreatingProjectLabel id: errorWhileCreatingProjectLabel
text: manager.projectCreationErrorText text: manager.projectCreationErrorText
width: parent.width width: parent.width
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("error") color: UM.Theme.getColor("error")
visible: manager.creatingNewProjectStatus == DF.RetrievalStatus.Failed visible: manager.creatingNewProjectStatus == DF.RetrievalStatus.Failed
anchors anchors

View file

@ -37,7 +37,7 @@ Cura.RoundedRectangle
width: UM.Theme.getSize("section").height width: UM.Theme.getSize("section").height
height: width height: width
color: UM.Theme.getColor("text_link") color: UM.Theme.getColor("text_link")
source: "../images/arrow_down.svg" source: Qt.resolvedUrl("../images/arrow_down.svg")
} }
Label Label
@ -65,7 +65,7 @@ Cura.RoundedRectangle
{ {
target: projectImage target: projectImage
color: UM.Theme.getColor("text_link") color: UM.Theme.getColor("text_link")
source: "../images/arrow_down.svg" source: Qt.resolvedUrl("../images/arrow_down.svg")
} }
PropertyChanges PropertyChanges
{ {
@ -88,7 +88,7 @@ Cura.RoundedRectangle
{ {
target: projectImage target: projectImage
color: UM.Theme.getColor("text_link") color: UM.Theme.getColor("text_link")
source: "../images/arrow_down.svg" source: Qt.resolvedUrl("../images/arrow_down.svg")
} }
PropertyChanges PropertyChanges
{ {
@ -111,7 +111,7 @@ Cura.RoundedRectangle
{ {
target: projectImage target: projectImage
color: UM.Theme.getColor("action_button_disabled_text") color: UM.Theme.getColor("action_button_disabled_text")
source: "../images/update.svg" source: Qt.resolvedUrl("../images/update.svg")
} }
PropertyChanges PropertyChanges
{ {

View file

@ -83,13 +83,12 @@ Item
} }
} }
Label UM.Label
{ {
id: emptyProjectLabel id: emptyProjectLabel
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "Select a project to view its files." text: "Select a project to view its files."
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("setting_category_text") color: UM.Theme.getColor("setting_category_text")
Connections Connections
@ -102,14 +101,13 @@ Item
} }
} }
Label UM.Label
{ {
id: noFilesInProjectLabel id: noFilesInProjectLabel
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: (manager.digitalFactoryFileModel.count == 0 && !emptyProjectLabel.visible && !retrievingFilesBusyIndicator.visible) visible: (manager.digitalFactoryFileModel.count == 0 && !emptyProjectLabel.visible && !retrievingFilesBusyIndicator.visible)
text: "No supported files in this project." text: "No supported files in this project."
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("setting_category_text") color: UM.Theme.getColor("setting_category_text")
} }

View file

@ -2,7 +2,7 @@
import QtQuick 2.10 import QtQuick 2.10
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import UM 1.2 as UM import UM 1.5 as UM
import Cura 1.6 as Cura import Cura 1.6 as Cura
Cura.RoundedRectangle Cura.RoundedRectangle
@ -58,34 +58,31 @@ Cura.RoundedRectangle
width: parent.width - x - UM.Theme.getSize("default_margin").width width: parent.width - x - UM.Theme.getSize("default_margin").width
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Label UM.Label
{ {
id: displayNameLabel id: displayNameLabel
width: parent.width width: parent.width
height: Math.round(parent.height / 3) height: Math.round(parent.height / 3)
elide: Text.ElideRight elide: Text.ElideRight
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default_bold") font: UM.Theme.getFont("default_bold")
} }
Label UM.Label
{ {
id: usernameLabel id: usernameLabel
width: parent.width width: parent.width
height: Math.round(parent.height / 3) height: Math.round(parent.height / 3)
elide: Text.ElideRight elide: Text.ElideRight
color: UM.Theme.getColor("small_button_text") color: UM.Theme.getColor("small_button_text")
font: UM.Theme.getFont("default")
} }
Label UM.Label
{ {
id: lastUpdatedLabel id: lastUpdatedLabel
width: parent.width width: parent.width
height: Math.round(parent.height / 3) height: Math.round(parent.height / 3)
elide: Text.ElideRight elide: Text.ElideRight
color: UM.Theme.getColor("small_button_text") color: UM.Theme.getColor("small_button_text")
font: UM.Theme.getFont("default")
} }
} }
} }

View file

@ -14,6 +14,9 @@ import DigitalFactory 1.0 as DF
Item Item
{ {
id: base id: base
property variant catalog: UM.I18nCatalog { name: "cura" }
width: parent.width width: parent.width
height: parent.height height: parent.height
@ -44,14 +47,13 @@ Item
cardMouseAreaEnabled: false cardMouseAreaEnabled: false
} }
Label UM.Label
{ {
id: fileNameLabel id: fileNameLabel
anchors.top: projectSummaryCard.bottom anchors.top: projectSummaryCard.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height anchors.topMargin: UM.Theme.getSize("default_margin").height
text: "Cura project name" text: "Cura project name"
font: UM.Theme.getFont("medium") font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
} }
@ -110,13 +112,12 @@ Item
} }
} }
Label UM.Label
{ {
id: emptyProjectLabel id: emptyProjectLabel
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "Select a project to view its files." text: "Select a project to view its files."
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("setting_category_text") color: UM.Theme.getColor("setting_category_text")
Connections Connections
@ -129,14 +130,13 @@ Item
} }
} }
Label UM.Label
{ {
id: noFilesInProjectLabel id: noFilesInProjectLabel
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: (manager.digitalFactoryFileModel.count == 0 && !emptyProjectLabel.visible && !retrievingFilesBusyIndicator.visible) visible: (manager.digitalFactoryFileModel.count == 0 && !emptyProjectLabel.visible && !retrievingFilesBusyIndicator.visible)
text: "No supported files in this project." text: "No supported files in this project."
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("setting_category_text") color: UM.Theme.getColor("setting_category_text")
} }
@ -193,53 +193,29 @@ Item
text: "Save" text: "Save"
enabled: (asProjectCheckbox.checked || asSlicedCheckbox.checked) && dfFilenameTextfield.text.length >= 1 && dfFilenameTextfield.state !== 'invalid' enabled: (asProjectCheckbox.checked || asSlicedCheckbox.checked) && dfFilenameTextfield.text.length >= 1 && dfFilenameTextfield.state !== 'invalid'
onClicked: onClicked: manager.saveFileToSelectedProject(dfFilenameTextfield.text, asProjectComboBox.currentValue)
{
let saveAsFormats = [];
if (asProjectCheckbox.checked)
{
saveAsFormats.push("3mf");
}
if (asSlicedCheckbox.checked)
{
saveAsFormats.push("ufp");
}
manager.saveFileToSelectedProject(dfFilenameTextfield.text, saveAsFormats);
}
busy: false busy: false
} }
Row Cura.ComboBox
{ {
id: asProjectComboBox
id: saveAsFormatRow width: UM.Theme.getSize("combobox_wide").width
height: saveButton.height
anchors.verticalCenter: saveButton.verticalCenter anchors.verticalCenter: saveButton.verticalCenter
anchors.right: saveButton.left anchors.right: saveButton.left
anchors.rightMargin: UM.Theme.getSize("thin_margin").height anchors.rightMargin: UM.Theme.getSize("thin_margin").height
width: childrenRect.width
spacing: UM.Theme.getSize("default_margin").width
UM.CheckBox
{
id: asProjectCheckbox
height: UM.Theme.getSize("checkbox").height
anchors.verticalCenter: parent.verticalCenter
checked: true
text: "Save Cura project"
font: UM.Theme.getFont("medium")
}
UM.CheckBox
{
id: asSlicedCheckbox
height: UM.Theme.getSize("checkbox").height
anchors.verticalCenter: parent.verticalCenter
enabled: UM.Backend.state == UM.Backend.Done enabled: UM.Backend.state == UM.Backend.Done
checked: UM.Backend.state == UM.Backend.Done currentIndex: UM.Backend.state == UM.Backend.Done ? 0 : 1
text: "Save print file" textRole: "text"
font: UM.Theme.getFont("medium") valueRole: "value"
}
model: [
{ text: catalog.i18nc("@option", "Save Cura project and print file"), key: "3mf_ufp", value: ["3mf", "ufp"] },
{ text: catalog.i18nc("@option", "Save Cura project"), key: "3mf", value: ["3mf"] },
]
} }
Component.onCompleted: Component.onCompleted:

View file

@ -99,18 +99,17 @@ Item
{ {
id: digitalFactoryImage id: digitalFactoryImage
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
source: searchBar.text === "" ? "../images/digital_factory.svg" : "../images/projects_not_found.svg" source: Qt.resolvedUrl(searchBar.text === "" ? "../images/digital_factory.svg" : "../images/projects_not_found.svg")
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
width: parent.width - 2 * UM.Theme.getSize("thick_margin").width width: parent.width - 2 * UM.Theme.getSize("thick_margin").width
} }
Label UM.Label
{ {
id: noLibraryProjectsLabel id: noLibraryProjectsLabel
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
text: searchBar.text === "" ? "It appears that you don't have any projects in the Library yet." : "No projects found that match the search query." text: searchBar.text === "" ? "It appears that you don't have any projects in the Library yet." : "No projects found that match the search query."
font: UM.Theme.getFont("medium") font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
} }
Cura.TertiaryButton Cura.TertiaryButton

View file

@ -1,5 +1,6 @@
# Copyright (c) 2021 Ultimaker B.V. # Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import json import json
import threading import threading
from json import JSONDecodeError from json import JSONDecodeError
@ -37,17 +38,18 @@ class DFFileExportAndUploadManager:
formats: List[str], formats: List[str],
on_upload_error: Callable[[], Any], on_upload_error: Callable[[], Any],
on_upload_success: Callable[[], Any], on_upload_success: Callable[[], Any],
on_upload_finished: Callable[[], Any] , on_upload_finished: Callable[[], Any],
on_upload_progress: Callable[[int], Any]) -> None: on_upload_progress: Callable[[int], Any]) -> None:
self._file_handlers = file_handlers # type: Dict[str, FileHandler] self._file_handlers: Dict[str, FileHandler] = file_handlers
self._nodes = nodes # type: List[SceneNode] self._nodes: List[SceneNode] = nodes
self._library_project_id = library_project_id # type: str self._library_project_id: str = library_project_id
self._library_project_name = library_project_name # type: str self._library_project_name: str = library_project_name
self._file_name = file_name # type: str self._file_name: str = file_name
self._upload_jobs = [] # type: List[ExportFileJob] self._upload_jobs: List[ExportFileJob] = []
self._formats = formats # type: List[str] self._formats: List[str] = formats
self._api = DigitalFactoryApiClient(application = CuraApplication.getInstance(), on_error = lambda error: Logger.log("e", str(error))) self._api = DigitalFactoryApiClient(application = CuraApplication.getInstance(), on_error = lambda error: Logger.log("e", str(error)))
self._source_file_id: Optional[str] = None
# Functions of the parent class that should be called based on the upload process output # Functions of the parent class that should be called based on the upload process output
self._on_upload_error = on_upload_error self._on_upload_error = on_upload_error
@ -59,7 +61,7 @@ class DFFileExportAndUploadManager:
# show the success message (once both upload jobs are done) # show the success message (once both upload jobs are done)
self._message_lock = threading.Lock() self._message_lock = threading.Lock()
self._file_upload_job_metadata = self.initializeFileUploadJobMetadata() # type: Dict[str, Dict[str, Any]] self._file_upload_job_metadata: Dict[str, Dict[str, Any]] = self.initializeFileUploadJobMetadata()
self.progress_message = Message( self.progress_message = Message(
title = "Uploading...", title = "Uploading...",
@ -113,7 +115,8 @@ class DFFileExportAndUploadManager:
content_type = job.getMimeType(), content_type = job.getMimeType(),
job_name = job.getFileName(), job_name = job.getFileName(),
file_size = len(job.getOutput()), file_size = len(job.getOutput()),
library_project_id = self._library_project_id library_project_id = self._library_project_id,
source_file_id = self._source_file_id
) )
self._api.requestUploadUFP(request, on_finished = self._uploadFileData, on_error = self._onRequestUploadPrintFileFailed) self._api.requestUploadUFP(request, on_finished = self._uploadFileData, on_error = self._onRequestUploadPrintFileFailed)
@ -125,11 +128,17 @@ class DFFileExportAndUploadManager:
""" """
if isinstance(file_upload_response, DFLibraryFileUploadResponse): if isinstance(file_upload_response, DFLibraryFileUploadResponse):
file_name = file_upload_response.file_name file_name = file_upload_response.file_name
# store the `file_id` so it can be as `source_file_id` when uploading the print file
self._source_file_id = file_upload_response.file_id
elif isinstance(file_upload_response, DFPrintJobUploadResponse): elif isinstance(file_upload_response, DFPrintJobUploadResponse):
file_name = file_upload_response.job_name if file_upload_response.job_name is not None else "" file_name = file_upload_response.job_name if file_upload_response.job_name is not None else ""
else: else:
Logger.log("e", "Wrong response type received. Aborting uploading file to the Digital Library") Logger.log("e", "Wrong response type received. Aborting uploading file to the Digital Library")
return return
if file_name not in self._file_upload_job_metadata:
Logger.error(f"API response for uploading doesn't match the file name we just uploaded: {file_name} was never uploaded.")
return
with self._message_lock: with self._message_lock:
self.progress_message.show() self.progress_message.show()
self._file_upload_job_metadata[file_name]["file_upload_response"] = file_upload_response self._file_upload_job_metadata[file_name]["file_upload_response"] = file_upload_response
@ -145,6 +154,8 @@ class DFFileExportAndUploadManager:
on_progress = self._onUploadProgress, on_progress = self._onUploadProgress,
on_error = self._onUploadError) on_error = self._onUploadError)
self._handleNextUploadJob()
def _onUploadProgress(self, filename: str, progress: int) -> None: def _onUploadProgress(self, filename: str, progress: int) -> None:
""" """
Updates the progress message according to the total progress of the two files and displays it to the user. It is Updates the progress message according to the total progress of the two files and displays it to the user. It is
@ -325,8 +336,14 @@ class DFFileExportAndUploadManager:
message.hide() message.hide()
def start(self) -> None: def start(self) -> None:
for job in self._upload_jobs: self._handleNextUploadJob()
def _handleNextUploadJob(self):
try:
job = self._upload_jobs.pop(0)
job.start() job.start()
except IndexError:
pass # Empty list, do nothing.
def initializeFileUploadJobMetadata(self) -> Dict[str, Any]: def initializeFileUploadJobMetadata(self) -> Dict[str, Any]:
metadata = {} metadata = {}

View file

@ -39,8 +39,8 @@ class DFFileUploader:
:param on_error: The method to be called when an error occurs. :param on_error: The method to be called when an error occurs.
""" """
self._http = http # type: HttpRequestManager self._http: HttpRequestManager = http
self._df_file = df_file # type: Union[DFLibraryFileUploadResponse, DFPrintJobUploadResponse] self._df_file: Union[DFLibraryFileUploadResponse, DFPrintJobUploadResponse] = df_file
self._file_name = "" self._file_name = ""
if isinstance(self._df_file, DFLibraryFileUploadResponse): if isinstance(self._df_file, DFLibraryFileUploadResponse):
self._file_name = self._df_file.file_name self._file_name = self._df_file.file_name
@ -51,7 +51,7 @@ class DFFileUploader:
self._file_name = "" self._file_name = ""
else: else:
raise TypeError("Incorrect input type") raise TypeError("Incorrect input type")
self._data = data # type: bytes self._data: bytes = data
self._on_finished = on_finished self._on_finished = on_finished
self._on_success = on_success self._on_success = on_success

View file

@ -1,12 +1,14 @@
# Copyright (c) 2021 Ultimaker B.V. # Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
from .BaseModel import BaseModel from .BaseModel import BaseModel
# Model that represents the request to upload a print job to the cloud # Model that represents the request to upload a print job to the cloud
class DFPrintJobUploadRequest(BaseModel): class DFPrintJobUploadRequest(BaseModel):
def __init__(self, job_name: str, file_size: int, content_type: str, library_project_id: str, **kwargs) -> None: def __init__(self, job_name: str, file_size: int, content_type: str, library_project_id: str, source_file_id: str, **kwargs) -> None:
"""Creates a new print job upload request. """Creates a new print job upload request.
:param job_name: The name of the print job. :param job_name: The name of the print job.
@ -18,4 +20,5 @@ class DFPrintJobUploadRequest(BaseModel):
self.file_size = file_size self.file_size = file_size
self.content_type = content_type self.content_type = content_type
self.library_project_id = library_project_id self.library_project_id = library_project_id
self.source_file_id = source_file_id
super().__init__(**kwargs) super().__init__(**kwargs)

View file

@ -40,7 +40,7 @@ class DigitalFactoryApiClient:
DEFAULT_REQUEST_TIMEOUT = 10 # seconds DEFAULT_REQUEST_TIMEOUT = 10 # seconds
# In order to avoid garbage collection we keep the callbacks in this list. # In order to avoid garbage collection we keep the callbacks in this list.
_anti_gc_callbacks = [] # type: List[Callable[[Any], None]] _anti_gc_callbacks: List[Callable[[Any], None]] = []
def __init__(self, application: CuraApplication, on_error: Callable[[List[CloudError]], None], projects_limit_per_page: Optional[int] = None) -> None: def __init__(self, application: CuraApplication, on_error: Callable[[List[CloudError]], None], projects_limit_per_page: Optional[int] = None) -> None:
"""Initializes a new digital factory API client. """Initializes a new digital factory API client.
@ -54,7 +54,7 @@ class DigitalFactoryApiClient:
self._scope = JsonDecoratorScope(UltimakerCloudScope(application)) self._scope = JsonDecoratorScope(UltimakerCloudScope(application))
self._http = HttpRequestManager.getInstance() self._http = HttpRequestManager.getInstance()
self._on_error = on_error self._on_error = on_error
self._file_uploader = None # type: Optional[DFFileUploader] self._file_uploader: Optional[DFFileUploader] = None
self._library_max_private_projects: Optional[int] = None self._library_max_private_projects: Optional[int] = None
self._projects_pagination_mgr = PaginationManager(limit = projects_limit_per_page) if projects_limit_per_page else None # type: Optional[PaginationManager] self._projects_pagination_mgr = PaginationManager(limit = projects_limit_per_page) if projects_limit_per_page else None # type: Optional[PaginationManager]

View file

@ -5,7 +5,7 @@ import QtQuick 2.2
import QtQuick.Controls 2.1 import QtQuick.Controls 2.1
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.1
import QtQuick.Window 2.1 import QtQuick.Window 2.1
import QtQuick.Dialogs 6.0 // For filedialog import QtQuick.Dialogs // For filedialog
import UM 1.5 as UM import UM 1.5 as UM
import Cura 1.0 as Cura import Cura 1.0 as Cura
@ -147,8 +147,6 @@ Cura.MachineAction
return catalog.i18nc("@label","Firmware update failed due to missing firmware."); return catalog.i18nc("@label","Firmware update failed due to missing firmware.");
} }
} }
wrapMode: Text.Wrap
} }
UM.ProgressBar UM.ProgressBar

View file

@ -31,7 +31,7 @@ UM.Dialog
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: UM.Theme.getSize("setting_control").width Layout.minimumWidth: UM.Theme.getSize("setting_control").width
text: catalog.i18nc("@action:label", "Height (mm)") text: catalog.i18nc("@action:label", "Height (mm)")
Layout.alignment: Qt.AlignmentFlag.AlignVCenter Layout.alignment: Qt.AlignVCenter
MouseArea { MouseArea {
id: peak_height_label id: peak_height_label
@ -64,7 +64,7 @@ UM.Dialog
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: UM.Theme.getSize("setting_control").width Layout.minimumWidth: UM.Theme.getSize("setting_control").width
text: catalog.i18nc("@action:label", "Base (mm)") text: catalog.i18nc("@action:label", "Base (mm)")
Layout.alignment: Qt.AlignmentFlag.AlignVCenter Layout.alignment: Qt.AlignVCenter
MouseArea MouseArea
{ {
@ -98,7 +98,7 @@ UM.Dialog
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: UM.Theme.getSize("setting_control").width Layout.minimumWidth: UM.Theme.getSize("setting_control").width
text: catalog.i18nc("@action:label", "Width (mm)") text: catalog.i18nc("@action:label", "Width (mm)")
Layout.alignment: Qt.AlignmentFlag.AlignVCenter Layout.alignment: Qt.AlignVCenter
MouseArea { MouseArea {
id: width_label id: width_label
@ -132,7 +132,7 @@ UM.Dialog
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: UM.Theme.getSize("setting_control").width Layout.minimumWidth: UM.Theme.getSize("setting_control").width
text: catalog.i18nc("@action:label", "Depth (mm)") text: catalog.i18nc("@action:label", "Depth (mm)")
Layout.alignment: Qt.AlignmentFlag.AlignVCenter Layout.alignment: Qt.AlignVCenter
MouseArea { MouseArea {
id: depth_label id: depth_label
@ -166,7 +166,7 @@ UM.Dialog
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: UM.Theme.getSize("setting_control").width Layout.minimumWidth: UM.Theme.getSize("setting_control").width
text: "" text: ""
Layout.alignment: Qt.AlignmentFlag.AlignVCenter Layout.alignment: Qt.AlignVCenter
MouseArea { MouseArea {
id: lighter_is_higher_label id: lighter_is_higher_label
@ -203,7 +203,7 @@ UM.Dialog
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: UM.Theme.getSize("setting_control").width Layout.minimumWidth: UM.Theme.getSize("setting_control").width
text: catalog.i18nc("@action:label", "Color Model") text: catalog.i18nc("@action:label", "Color Model")
Layout.alignment: Qt.AlignmentFlag.AlignVCenter Layout.alignment: Qt.AlignVCenter
MouseArea { MouseArea {
id: color_model_label id: color_model_label
@ -240,7 +240,7 @@ UM.Dialog
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: UM.Theme.getSize("setting_control").width Layout.minimumWidth: UM.Theme.getSize("setting_control").width
text: catalog.i18nc("@action:label", "1mm Transmittance (%)") text: catalog.i18nc("@action:label", "1mm Transmittance (%)")
Layout.alignment: Qt.AlignmentFlag.AlignVCenter Layout.alignment: Qt.AlignVCenter
MouseArea { MouseArea {
id: transmittance_label id: transmittance_label
@ -272,7 +272,7 @@ UM.Dialog
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: UM.Theme.getSize("setting_control").width Layout.minimumWidth: UM.Theme.getSize("setting_control").width
text: catalog.i18nc("@action:label", "Smoothing") text: catalog.i18nc("@action:label", "Smoothing")
Layout.alignment: Qt.AlignmentFlag.AlignVCenter Layout.alignment: Qt.AlignVCenter
MouseArea MouseArea
{ {

View file

@ -4,7 +4,7 @@
import QtQuick 2.10 import QtQuick 2.10
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import UM 1.3 as UM import UM 1.5 as UM
import Cura 1.1 as Cura import Cura 1.1 as Cura
@ -58,11 +58,10 @@ Item
spacing: base.columnSpacing spacing: base.columnSpacing
Label // Title Label UM.Label // Title Label
{ {
text: catalog.i18nc("@title:label", "Nozzle Settings") text: catalog.i18nc("@title:label", "Nozzle Settings")
font: UM.Theme.getFont("medium_bold") font: UM.Theme.getFont("medium_bold")
renderType: Text.NativeRendering
} }
Cura.NumericTextFieldWithUnit // "Nozzle size" Cura.NumericTextFieldWithUnit // "Nozzle size"

View file

@ -5,7 +5,7 @@ import QtQuick 2.10
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import UM 1.3 as UM import UM 1.5 as UM
import Cura 1.1 as Cura import Cura 1.1 as Cura
@ -47,16 +47,14 @@ Item
Column Column
{ {
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignmentFlag.AlignTop Layout.alignment: Qt.AlignTop
spacing: base.columnSpacing spacing: base.columnSpacing
Label // Title Label UM.Label // Title Label
{ {
text: catalog.i18nc("@title:label", "Printer Settings") text: catalog.i18nc("@title:label", "Printer Settings")
font: UM.Theme.getFont("medium_bold") font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
width: parent.width width: parent.width
elide: Text.ElideRight elide: Text.ElideRight
} }
@ -178,16 +176,14 @@ Item
Column Column
{ {
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignmentFlag.AlignTop Layout.alignment: Qt.AlignTop
spacing: base.columnSpacing spacing: base.columnSpacing
Label // Title Label UM.Label // Title Label
{ {
text: catalog.i18nc("@title:label", "Printhead Settings") text: catalog.i18nc("@title:label", "Printhead Settings")
font: UM.Theme.getFont("medium_bold") font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
width: parent.width width: parent.width
elide: Text.ElideRight elide: Text.ElideRight
} }

View file

@ -0,0 +1,67 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
from PyQt6.QtCore import QObject, pyqtSignal, pyqtProperty
from typing import Optional, List, Dict, cast, Callable
from cura.CuraApplication import CuraApplication
from UM.PluginRegistry import PluginRegistry
from cura.CuraPackageManager import CuraPackageManager
from UM.i18n import i18nCatalog
from UM.FlameProfiler import pyqtSlot
from .MissingPackageList import MissingPackageList
i18n_catalog = i18nCatalog("cura")
class InstallMissingPackageDialog(QObject):
"""Dialog used to display packages that need to be installed to load 3mf file materials"""
def __init__(self, packages_metadata: List[Dict[str, str]], show_missing_materials_warning: Callable[[], None]) -> None:
"""Initialize
:param packages_metadata: List of dictionaries containing information about missing packages.
"""
super().__init__()
self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry()
self._package_manager: CuraPackageManager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager())
self._package_manager.installedPackagesChanged.connect(self.checkIfRestartNeeded)
self._dialog: Optional[QObject] = None
self._restart_needed = False
self._package_metadata: List[Dict[str, str]] = packages_metadata
self._package_model: MissingPackageList = MissingPackageList(packages_metadata)
self._show_missing_materials_warning = show_missing_materials_warning
def show(self) -> None:
plugin_path = self._plugin_registry.getPluginPath("Marketplace")
if plugin_path is None:
plugin_path = os.path.dirname(__file__)
# create a QML component for the license dialog
license_dialog_component_path = os.path.join(plugin_path, "resources", "qml", "InstallMissingPackagesDialog.qml")
self._dialog = CuraApplication.getInstance().createQmlComponent(license_dialog_component_path, {"manager": self})
self._dialog.show()
def checkIfRestartNeeded(self) -> None:
if self._dialog is None:
return
self._restart_needed = self._package_manager.hasPackagesToRemoveOrInstall
self.showRestartChanged.emit()
showRestartChanged = pyqtSignal()
@pyqtProperty(bool, notify=showRestartChanged)
def showRestartNotification(self) -> bool:
return self._restart_needed
@pyqtProperty(QObject)
def model(self) -> MissingPackageList:
return self._package_model
@pyqtSlot()
def showMissingMaterialsWarning(self) -> None:
self._show_missing_materials_warning()

View file

@ -108,7 +108,7 @@ class LocalPackageList(PackageList):
:param reply: A reply containing information about a number of packages. :param reply: A reply containing information about a number of packages.
""" """
response_data = HttpRequestManager.readJSON(reply) response_data = HttpRequestManager.readJSON(reply)
if "data" not in response_data: if response_data is None or "data" not in response_data:
Logger.error( Logger.error(
f"Could not interpret the server's response. Missing 'data' from response data. Keys in response: {response_data.keys()}") f"Could not interpret the server's response. Missing 'data' from response data. Keys in response: {response_data.keys()}")
return return

View file

@ -3,15 +3,15 @@
import os.path import os.path
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from typing import Optional, cast from typing import Callable, cast, Dict, List, Optional
from cura.CuraApplication import CuraApplication # Creating QML objects and managing packages. from cura.CuraApplication import CuraApplication # Creating QML objects and managing packages.
from UM.Extension import Extension # We are implementing the main object of an extension here. from UM.Extension import Extension # We are implementing the main object of an extension here.
from UM.PluginRegistry import PluginRegistry # To find out where we are stored (the proper way). from UM.PluginRegistry import PluginRegistry # To find out where we are stored (the proper way).
from .RemotePackageList import RemotePackageList # To register this type with QML. from .InstallMissingPackagesDialog import InstallMissingPackageDialog # To allow creating this dialogue from outside of the plug-in.
from .LocalPackageList import LocalPackageList # To register this type with QML. from .LocalPackageList import LocalPackageList # To register this type with QML.
from .RemotePackageList import RemotePackageList # To register this type with QML.
class Marketplace(Extension, QObject): class Marketplace(Extension, QObject):
@ -22,7 +22,6 @@ class Marketplace(Extension, QObject):
QObject.__init__(self, parent) QObject.__init__(self, parent)
Extension.__init__(self) Extension.__init__(self)
self._window: Optional["QObject"] = None # If the window has been loaded yet, it'll be cached in here. self._window: Optional["QObject"] = None # If the window has been loaded yet, it'll be cached in here.
self._plugin_registry: Optional[PluginRegistry] = None
self._package_manager = CuraApplication.getInstance().getPackageManager() self._package_manager = CuraApplication.getInstance().getPackageManager()
self._material_package_list: Optional[RemotePackageList] = None self._material_package_list: Optional[RemotePackageList] = None
@ -41,6 +40,7 @@ class Marketplace(Extension, QObject):
self._tab_shown: int = 0 self._tab_shown: int = 0
self._restart_needed = False self._restart_needed = False
self.missingPackageDialog = None
def getTabShown(self) -> int: def getTabShown(self) -> int:
return self._tab_shown return self._tab_shown
@ -80,9 +80,9 @@ class Marketplace(Extension, QObject):
If the window hadn't been loaded yet into Qt, it will be created lazily. If the window hadn't been loaded yet into Qt, it will be created lazily.
""" """
if self._window is None: if self._window is None:
self._plugin_registry = PluginRegistry.getInstance() plugin_registry = PluginRegistry.getInstance()
self._plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.checkIfRestartNeeded) plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.checkIfRestartNeeded)
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId()) plugin_path = plugin_registry.getPluginPath(self.getPluginId())
if plugin_path is None: if plugin_path is None:
plugin_path = os.path.dirname(__file__) plugin_path = os.path.dirname(__file__)
path = os.path.join(plugin_path, "resources", "qml", "Marketplace.qml") path = os.path.join(plugin_path, "resources", "qml", "Marketplace.qml")
@ -103,8 +103,11 @@ class Marketplace(Extension, QObject):
self.setTabShown(1) self.setTabShown(1)
def checkIfRestartNeeded(self) -> None: def checkIfRestartNeeded(self) -> None:
if self._window is None:
return
if self._package_manager.hasPackagesToRemoveOrInstall or \ if self._package_manager.hasPackagesToRemoveOrInstall or \
cast(PluginRegistry, self._plugin_registry).getCurrentSessionActivationChangedPlugins(): PluginRegistry.getInstance().getCurrentSessionActivationChangedPlugins():
self._restart_needed = True self._restart_needed = True
else: else:
self._restart_needed = False self._restart_needed = False
@ -112,6 +115,18 @@ class Marketplace(Extension, QObject):
showRestartNotificationChanged = pyqtSignal() showRestartNotificationChanged = pyqtSignal()
@pyqtProperty(bool, notify=showRestartNotificationChanged) @pyqtProperty(bool, notify = showRestartNotificationChanged)
def showRestartNotification(self) -> bool: def showRestartNotification(self) -> bool:
return self._restart_needed return self._restart_needed
def showInstallMissingPackageDialog(self, packages_metadata: List[Dict[str, str]], ignore_warning_callback: Callable[[], None]) -> None:
"""
Show a dialog that prompts the user to install certain packages.
The dialog is worded for packages that are missing and required for a certain operation.
:param packages_metadata: The metadata of the packages that are missing.
:param ignore_warning_callback: A callback that gets executed when the user ignores the pop-up, to show them a
warning.
"""
self.missingPackageDialog = InstallMissingPackageDialog(packages_metadata, ignore_warning_callback)
self.missingPackageDialog.show()

View file

@ -0,0 +1,46 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, TYPE_CHECKING, Dict, List
from .Constants import PACKAGES_URL
from .PackageModel import PackageModel
from .RemotePackageList import RemotePackageList
from PyQt6.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication
from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To request the package list from the API.
from UM.i18n import i18nCatalog
if TYPE_CHECKING:
from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal
catalog = i18nCatalog("cura")
class MissingPackageList(RemotePackageList):
def __init__(self, packages_metadata: List[Dict[str, str]], parent: Optional["QObject"] = None) -> None:
super().__init__(parent)
self._packages_metadata: List[Dict[str, str]] = packages_metadata
self._package_type_filter = "material"
self._search_type = "package_ids"
self._requested_search_string = ",".join(map(lambda package: package["id"], packages_metadata))
def _parseResponse(self, reply: "QNetworkReply") -> None:
super()._parseResponse(reply)
# At the end of the list we want to show some information about packages the user is missing that can't be found
# This will add cards with some information about the missing packages
if not self.hasMore:
self._addPackagesMissingFromRequest()
def _addPackagesMissingFromRequest(self) -> None:
"""Create cards for packages the user needs to install that could not be found"""
returned_packages_ids = [item["package"].packageId for item in self._items]
for package_metadata in self._packages_metadata:
if package_metadata["id"] not in returned_packages_ids:
package = PackageModel.fromIncompletePackageInformation(package_metadata["display_name"], package_metadata["package_version"], self._package_type_filter)
self.appendItem({"package": package})
self.itemsChanged.emit()

View file

@ -244,7 +244,10 @@ class PackageList(ListModel):
def _downloadError(self, package_id: str, update: bool = False, reply: Optional["QNetworkReply"] = None, error: Optional["QNetworkReply.NetworkError"] = None) -> None: def _downloadError(self, package_id: str, update: bool = False, reply: Optional["QNetworkReply"] = None, error: Optional["QNetworkReply.NetworkError"] = None) -> None:
if reply: if reply:
try:
reply_string = bytes(reply.readAll()).decode() reply_string = bytes(reply.readAll()).decode()
except UnicodeDecodeError:
reply_string = "<error message is corrupt too>"
Logger.error(f"Failed to download package: {package_id} due to {reply_string}") Logger.error(f"Failed to download package: {package_id} due to {reply_string}")
self._package_manager.packageInstallingFailed.emit(package_id) self._package_manager.packageInstallingFailed.emit(package_id)

View file

@ -84,6 +84,20 @@ class PackageModel(QObject):
self._is_busy = False self._is_busy = False
self._is_missing_package_information = False
@classmethod
def fromIncompletePackageInformation(cls, display_name: str, package_version: str, package_type: str) -> "PackageModel":
package_data = {
"display_name": display_name,
"package_version": package_version,
"package_type": package_type,
"description": "The material package associated with the Cura project could not be found on the Ultimaker marketplace. Use the partial material profile definition stored in the Cura project file at your own risk."
}
package_model = cls(package_data)
package_model.setIsMissingPackageInformation(True)
return package_model
@pyqtSlot() @pyqtSlot()
def _processUpdatedPackages(self): def _processUpdatedPackages(self):
self.setCanUpdate(self._package_manager.checkIfPackageCanUpdate(self._package_id)) self.setCanUpdate(self._package_manager.checkIfPackageCanUpdate(self._package_id))
@ -385,3 +399,14 @@ class PackageModel(QObject):
def canUpdate(self) -> bool: def canUpdate(self) -> bool:
"""Flag indicating if the package can be updated""" """Flag indicating if the package can be updated"""
return self._can_update return self._can_update
isMissingPackageInformationChanged = pyqtSignal()
def setIsMissingPackageInformation(self, isMissingPackageInformation: bool) -> None:
self._is_missing_package_information = isMissingPackageInformation
self.isMissingPackageInformationChanged.emit()
@pyqtProperty(bool, notify=isMissingPackageInformationChanged)
def isMissingPackageInformation(self) -> bool:
"""Flag indicating if the package can be updated"""
return self._is_missing_package_information

View file

@ -28,6 +28,7 @@ class RemotePackageList(PackageList):
self._package_type_filter = "" self._package_type_filter = ""
self._requested_search_string = "" self._requested_search_string = ""
self._current_search_string = "" self._current_search_string = ""
self._search_type = "search"
self._request_url = self._initialRequestUrl() self._request_url = self._initialRequestUrl()
self._ongoing_requests["get_packages"] = None self._ongoing_requests["get_packages"] = None
self.isLoadingChanged.connect(self._onLoadingChanged) self.isLoadingChanged.connect(self._onLoadingChanged)
@ -100,7 +101,7 @@ class RemotePackageList(PackageList):
if self._package_type_filter != "": if self._package_type_filter != "":
request_url += f"&package_type={self._package_type_filter}" request_url += f"&package_type={self._package_type_filter}"
if self._current_search_string != "": if self._current_search_string != "":
request_url += f"&search={self._current_search_string}" request_url += f"&{self._search_type}={self._current_search_string}"
return request_url return request_url
def _parseResponse(self, reply: "QNetworkReply") -> None: def _parseResponse(self, reply: "QNetworkReply") -> None:

View file

@ -14,4 +14,4 @@ def register(app):
""" """
Register the plug-in object with Uranium. Register the plug-in object with Uranium.
""" """
return { "extension": [Marketplace(), SyncOrchestrator(app)] } return { "extension": [SyncOrchestrator(app), Marketplace()] }

View file

@ -67,7 +67,7 @@ UM.Dialog
Image Image
{ {
id: packageIcon id: packageIcon
source: model.icon_url || Qt.resolvedUrl("../../images/placeholder.svg") source: model.icon_url || Qt.resolvedUrl("../images/placeholder.svg")
height: lineHeight height: lineHeight
width: height width: height
sourceSize.height: height sourceSize.height: height
@ -109,7 +109,7 @@ UM.Dialog
Image Image
{ {
id: packageIcon id: packageIcon
source: model.icon_url || Qt.resolvedUrl("../../images/placeholder.svg") source: model.icon_url || Qt.resolvedUrl("../images/placeholder.svg")
height: lineHeight height: lineHeight
width: height width: height
sourceSize.height: height sourceSize.height: height

View file

@ -0,0 +1,21 @@
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.2
import UM 1.5 as UM
import Cura 1.6 as Cura
Marketplace
{
modality: Qt.ApplicationModal
title: catalog.i18nc("@title", "Install missing Materials")
pageContentsSource: "MissingPackages.qml"
showSearchHeader: false
showOnboadBanner: false
onClosing: manager.showMissingMaterialsWarning()
}

View file

@ -42,16 +42,13 @@ UM.Dialog
source: UM.Theme.getIcon("Certificate", "high") source: UM.Theme.getIcon("Certificate", "high")
} }
Label UM.Label
{ {
text: catalog.i18nc("@text", "Please read and agree with the plugin licence.") text: catalog.i18nc("@text", "Please read and agree with the plugin licence.")
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("large") font: UM.Theme.getFont("large")
anchors.verticalCenter: icon.verticalCenter anchors.verticalCenter: icon.verticalCenter
height: UM.Theme.getSize("marketplace_large_icon").height height: UM.Theme.getSize("marketplace_large_icon").height
verticalAlignment: Qt.AlignmentFlag.AlignVCenter verticalAlignment: Qt.AlignVCenter
wrapMode: Text.Wrap
renderType: Text.NativeRendering
} }
} }

View file

@ -6,7 +6,7 @@ import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import UM 1.2 as UM import UM 1.5 as UM
import Cura 1.6 as Cura import Cura 1.6 as Cura
Window Window
@ -16,6 +16,10 @@ Window
signal searchStringChanged(string new_search) signal searchStringChanged(string new_search)
property alias showOnboadBanner: onBoardBanner.visible
property alias showSearchHeader: searchHeader.visible
property alias pageContentsSource: content.source
minimumWidth: UM.Theme.getSize("modal_window_minimum").width minimumWidth: UM.Theme.getSize("modal_window_minimum").width
minimumHeight: UM.Theme.getSize("modal_window_minimum").height minimumHeight: UM.Theme.getSize("modal_window_minimum").height
width: minimumWidth width: minimumWidth
@ -67,7 +71,7 @@ Window
Layout.preferredWidth: parent.width Layout.preferredWidth: parent.width
Layout.preferredHeight: childrenRect.height + UM.Theme.getSize("default_margin").height Layout.preferredHeight: childrenRect.height + UM.Theme.getSize("default_margin").height
Label UM.Label
{ {
id: pageTitle id: pageTitle
anchors anchors
@ -80,13 +84,13 @@ Window
} }
font: UM.Theme.getFont("large") font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")
text: content.item ? content.item.pageTitle: catalog.i18nc("@title", "Loading...") text: content.item ? content.item.pageTitle: catalog.i18nc("@title", "Loading...")
} }
} }
OnboardBanner OnboardBanner
{ {
id: onBoardBanner
visible: content.item && content.item.bannerVisible visible: content.item && content.item.bannerVisible
text: content.item && content.item.bannerText text: content.item && content.item.bannerText
icon: content.item && content.item.bannerIcon icon: content.item && content.item.bannerIcon
@ -101,6 +105,7 @@ Window
// Search & Top-Level Tabs // Search & Top-Level Tabs
Item Item
{ {
id: searchHeader
implicitHeight: childrenRect.height implicitHeight: childrenRect.height
implicitWidth: parent.width - 2 * UM.Theme.getSize("default_margin").width implicitWidth: parent.width - 2 * UM.Theme.getSize("default_margin").width
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
@ -187,7 +192,7 @@ Window
{ {
text: catalog.i18nc("@info", "Search in the browser") text: catalog.i18nc("@info", "Search in the browser")
iconSource: UM.Theme.getIcon("LinkExternal") iconSource: UM.Theme.getIcon("LinkExternal")
visible: pageSelectionTabBar.currentItem.hasSearch visible: pageSelectionTabBar.currentItem.hasSearch && searchHeader.visible
isIconOnRightSide: true isIconOnRightSide: true
height: fontMetrics.height height: fontMetrics.height
textFont: fontMetrics.font textFont: fontMetrics.font

View file

@ -0,0 +1,15 @@
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import UM 1.4 as UM
Packages
{
pageTitle: catalog.i18nc("@header", "Install Materials")
bannerVisible: false
showUpdateButton: false
showInstallButton: true
model: manager.model
}

View file

@ -51,7 +51,7 @@ UM.Dialog
sourceSize.width: width sourceSize.width: width
sourceSize.height: height sourceSize.height: height
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
source: licenseModel.iconUrl || Qt.resolvedUrl("../../images/placeholder.svg") source: licenseModel.iconUrl || Qt.resolvedUrl("../images/placeholder.svg")
mipmap: true mipmap: true
} }

View file

@ -30,6 +30,8 @@ Rectangle
} }
width: UM.Theme.getSize("banner_icon_size").width width: UM.Theme.getSize("banner_icon_size").width
height: UM.Theme.getSize("banner_icon_size").height height: UM.Theme.getSize("banner_icon_size").height
color: UM.Theme.getColor("primary_text")
} }
UM.SimpleButton UM.SimpleButton

View file

@ -18,6 +18,8 @@ Rectangle
height: childrenRect.height height: childrenRect.height
color: UM.Theme.getColor("main_background") color: UM.Theme.getColor("main_background")
radius: UM.Theme.getSize("default_radius").width radius: UM.Theme.getSize("default_radius").width
border.color: packageData.isMissingPackageInformation ? UM.Theme.getColor("warning") : "transparent"
border.width: packageData.isMissingPackageInformation ? UM.Theme.getSize("default_lining").width : 0
PackageCardHeader PackageCardHeader
{ {
@ -29,16 +31,13 @@ Rectangle
anchors.fill: parent anchors.fill: parent
Label UM.Label
{ {
id: descriptionLabel id: descriptionLabel
width: parent.width width: parent.width
text: packageData.description text: packageData.description
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
maximumLineCount: 2 maximumLineCount: 2
wrapMode: Text.Wrap
elide: Text.ElideRight elide: Text.ElideRight
visible: text !== "" visible: text !== ""
} }

View file

@ -19,6 +19,8 @@ Item
property bool showInstallButton: false property bool showInstallButton: false
property bool showUpdateButton: false property bool showUpdateButton: false
property string missingPackageReadMoreUrl: "https://support.ultimaker.com/hc/en-us/articles/360011968360-Using-the-Ultimaker-Marketplace?utm_source=cura&utm_medium=software&utm_campaign=load-file-material-missing"
width: parent.width width: parent.width
height: UM.Theme.getSize("card").height height: UM.Theme.getSize("card").height
@ -87,11 +89,18 @@ Item
Layout.preferredWidth: parent.width Layout.preferredWidth: parent.width
Layout.preferredHeight: childrenRect.height Layout.preferredHeight: childrenRect.height
Label UM.StatusIcon
{
width: UM.Theme.getSize("section_icon").width + UM.Theme.getSize("narrow_margin").width
height: UM.Theme.getSize("section_icon").height
status: UM.StatusIcon.Status.WARNING
visible: packageData.isMissingPackageInformation
}
UM.Label
{ {
text: packageData.displayName text: packageData.displayName
font: UM.Theme.getFont("medium_bold") font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignTop verticalAlignment: Text.AlignTop
} }
VerifiedIcon VerifiedIcon
@ -100,18 +109,17 @@ Item
visible: packageData.isCheckedByUltimaker visible: packageData.isCheckedByUltimaker
} }
Label UM.Label
{ {
id: packageVersionLabel id: packageVersionLabel
text: packageData.packageVersion text: packageData.packageVersion
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
Layout.fillWidth: true Layout.fillWidth: true
} }
Button Button
{ {
id: externalLinkButton id: externalLinkButton
visible: !packageData.isMissingPackageInformation
// For some reason if i set padding, they don't match up. If i set all of them explicitly, it does work? // For some reason if i set padding, they don't match up. If i set all of them explicitly, it does work?
leftPadding: UM.Theme.getSize("narrow_margin").width leftPadding: UM.Theme.getSize("narrow_margin").width
@ -119,8 +127,8 @@ Item
topPadding: UM.Theme.getSize("narrow_margin").width topPadding: UM.Theme.getSize("narrow_margin").width
bottomPadding: UM.Theme.getSize("narrow_margin").width bottomPadding: UM.Theme.getSize("narrow_margin").width
Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width + 2 * padding width: UM.Theme.getSize("card_tiny_icon").width + 2 * padding
Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").width + 2 * padding height: UM.Theme.getSize("card_tiny_icon").width + 2 * padding
contentItem: UM.ColorImage contentItem: UM.ColorImage
{ {
source: UM.Theme.getIcon("LinkExternal") source: UM.Theme.getIcon("LinkExternal")
@ -155,12 +163,13 @@ Item
spacing: UM.Theme.getSize("narrow_margin").width spacing: UM.Theme.getSize("narrow_margin").width
// label "By" // label "By"
Label UM.Label
{ {
id: authorBy id: authorBy
visible: !packageData.isMissingPackageInformation
Layout.alignment: Qt.AlignCenter Layout.alignment: Qt.AlignCenter
text: catalog.i18nc("@label", "By") text: catalog.i18nc("@label Is followed by the name of an author", "By")
font: UM.Theme.getFont("default") font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")
} }
@ -168,6 +177,7 @@ Item
// clickable author name // clickable author name
Item Item
{ {
visible: !packageData.isMissingPackageInformation
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: authorBy.height implicitHeight: authorBy.height
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
@ -185,10 +195,29 @@ Item
} }
} }
Item
{
visible: packageData.isMissingPackageInformation
Layout.fillWidth: true
implicitHeight: readMoreButton.height
Layout.alignment: Qt.AlignTop
Cura.TertiaryButton
{
id: readMoreButton
text: catalog.i18nc("@button:label", "Learn More")
leftPadding: 0
rightPadding: 0
iconSource: UM.Theme.getIcon("LinkExternal")
isIconOnRightSide: true
onClicked: Qt.openUrlExternally(missingPackageReadMoreUrl)
}
}
ManageButton ManageButton
{ {
id: enableManageButton id: enableManageButton
visible: showDisableButton && packageData.isInstalled && !packageData.isToBeInstalled && packageData.packageType != "material" visible: showDisableButton && packageData.isInstalled && !packageData.isToBeInstalled && packageData.packageType != "material" && !packageData.isMissingPackageInformation
enabled: !packageData.busy enabled: !packageData.busy
button_style: !packageData.isActive button_style: !packageData.isActive
@ -202,7 +231,7 @@ Item
ManageButton ManageButton
{ {
id: installManageButton id: installManageButton
visible: showInstallButton && (packageData.canDowngrade || !packageData.isBundled) visible: showInstallButton && (packageData.canDowngrade || !packageData.isBundled) && !packageData.isMissingPackageInformation
enabled: !packageData.busy enabled: !packageData.busy
busy: packageData.busy busy: packageData.busy
button_style: !(packageData.isInstalled || packageData.isToBeInstalled) button_style: !(packageData.isInstalled || packageData.isToBeInstalled)
@ -232,7 +261,7 @@ Item
ManageButton ManageButton
{ {
id: updateManageButton id: updateManageButton
visible: showUpdateButton && packageData.canUpdate visible: showUpdateButton && packageData.canUpdate && !packageData.isMissingPackageInformation
enabled: !packageData.busy enabled: !packageData.busy
busy: packageData.busy busy: packageData.busy
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop

View file

@ -31,7 +31,7 @@ Item
Cura.SecondaryButton Cura.SecondaryButton
{ {
Layout.alignment: Qt.AlignmentFlag.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: UM.Theme.getSize("action_button").height Layout.preferredHeight: UM.Theme.getSize("action_button").height
Layout.preferredWidth: height Layout.preferredWidth: height
@ -45,14 +45,13 @@ Item
iconSize: height - leftPadding * 2 iconSize: height - leftPadding * 2
} }
Label UM.Label
{ {
Layout.alignment: Qt.AlignmentFlag.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true Layout.fillWidth: true
text: detailPage.title text: detailPage.title
font: UM.Theme.getFont("large") font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")
} }
} }

View file

@ -56,13 +56,9 @@ Rectangle
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")
} }
Label UM.Label
{ {
anchors.verticalCenter: downloadsIcon.verticalCenter anchors.verticalCenter: downloadsIcon.verticalCenter
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default")
text: packageData.downloadCount text: packageData.downloadCount
} }
} }
@ -78,25 +74,22 @@ Rectangle
topPadding: 0 topPadding: 0
spacing: UM.Theme.getSize("default_margin").height spacing: UM.Theme.getSize("default_margin").height
Label UM.Label
{ {
width: parent.width - parent.padding * 2 width: parent.width - parent.padding * 2
text: catalog.i18nc("@header", "Description") text: catalog.i18nc("@header", "Description")
font: UM.Theme.getFont("medium_bold") font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight elide: Text.ElideRight
} }
Label UM.Label
{ {
width: parent.width - parent.padding * 2 width: parent.width - parent.padding * 2
text: packageData.formattedDescription text: packageData.formattedDescription
font: UM.Theme.getFont("medium") font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link") linkColor: UM.Theme.getColor("text_link")
wrapMode: Text.Wrap
textFormat: Text.RichText textFormat: Text.RichText
onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"]) onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"])
@ -110,13 +103,12 @@ Rectangle
visible: packageData.packageType === "material" visible: packageData.packageType === "material"
spacing: 0 spacing: 0
Label UM.Label
{ {
width: parent.width width: parent.width
text: catalog.i18nc("@header", "Compatible printers") text: catalog.i18nc("@header", "Compatible printers")
font: UM.Theme.getFont("medium_bold") font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight elide: Text.ElideRight
} }
@ -124,25 +116,23 @@ Rectangle
{ {
model: packageData.compatiblePrinters model: packageData.compatiblePrinters
Label UM.Label
{ {
width: compatiblePrinterColumn.width width: compatiblePrinterColumn.width
text: modelData text: modelData
font: UM.Theme.getFont("medium") font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight elide: Text.ElideRight
} }
} }
Label UM.Label
{ {
width: parent.width width: parent.width
visible: packageData.compatiblePrinters.length == 0 visible: packageData.compatiblePrinters.length == 0
text: "(" + catalog.i18nc("@info", "No compatibility information") + ")" text: "(" + catalog.i18nc("@info", "No compatibility information") + ")"
font: UM.Theme.getFont("medium") font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight elide: Text.ElideRight
} }
} }
@ -155,13 +145,12 @@ Rectangle
visible: packageData.packageType === "material" visible: packageData.packageType === "material"
spacing: 0 spacing: 0
Label UM.Label
{ {
width: parent.width width: parent.width
text: catalog.i18nc("@header", "Compatible support materials") text: catalog.i18nc("@header", "Compatible support materials")
font: UM.Theme.getFont("medium_bold") font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight elide: Text.ElideRight
} }
@ -169,25 +158,23 @@ Rectangle
{ {
model: packageData.compatibleSupportMaterials model: packageData.compatibleSupportMaterials
Label UM.Label
{ {
width: compatibleSupportMaterialColumn.width width: compatibleSupportMaterialColumn.width
text: modelData text: modelData
font: UM.Theme.getFont("medium") font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight elide: Text.ElideRight
} }
} }
Label UM.Label
{ {
width: parent.width width: parent.width
visible: packageData.compatibleSupportMaterials.length == 0 visible: packageData.compatibleSupportMaterials.length == 0
text: "(" + catalog.i18nc("@info No materials", "None") + ")" text: "(" + catalog.i18nc("@info No materials", "None") + ")"
font: UM.Theme.getFont("medium") font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight elide: Text.ElideRight
} }
} }
@ -199,23 +186,21 @@ Rectangle
visible: packageData.packageType === "material" visible: packageData.packageType === "material"
spacing: 0 spacing: 0
Label UM.Label
{ {
width: parent.width width: parent.width
text: catalog.i18nc("@header", "Compatible with Material Station") text: catalog.i18nc("@header", "Compatible with Material Station")
font: UM.Theme.getFont("medium_bold") font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight elide: Text.ElideRight
} }
Label UM.Label
{ {
width: parent.width width: parent.width
text: packageData.isCompatibleMaterialStation ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No") text: packageData.isCompatibleMaterialStation ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No")
font: UM.Theme.getFont("medium") font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight elide: Text.ElideRight
} }
} }
@ -227,23 +212,21 @@ Rectangle
visible: packageData.packageType === "material" visible: packageData.packageType === "material"
spacing: 0 spacing: 0
Label UM.Label
{ {
width: parent.width width: parent.width
text: catalog.i18nc("@header", "Optimized for Air Manager") text: catalog.i18nc("@header", "Optimized for Air Manager")
font: UM.Theme.getFont("medium_bold") font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight elide: Text.ElideRight
} }
Label UM.Label
{ {
width: parent.width width: parent.width
text: packageData.isCompatibleAirManager ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No") text: packageData.isCompatibleAirManager ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No")
font: UM.Theme.getFont("medium") font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight elide: Text.ElideRight
} }
} }

View file

@ -3,7 +3,7 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import UM 1.0 as UM import UM 1.5 as UM
TabButton TabButton
{ {
@ -22,11 +22,10 @@ TabButton
border.width: UM.Theme.getSize("thick_lining").width border.width: UM.Theme.getSize("thick_lining").width
} }
contentItem: Label contentItem: UM.Label
{ {
text: parent.text text: parent.text
font: UM.Theme.getFont("medium_bold") font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
width: contentWidth width: contentWidth
anchors.centerIn: parent anchors.centerIn: parent
} }

View file

@ -40,7 +40,7 @@ ListView
color: UM.Theme.getColor("detail_background") color: UM.Theme.getColor("detail_background")
Label UM.Label
{ {
id: sectionHeaderText id: sectionHeaderText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -48,7 +48,6 @@ ListView
text: section text: section
font: UM.Theme.getFont("large") font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")
} }
} }
@ -62,10 +61,13 @@ ListView
hoverEnabled: true hoverEnabled: true
onClicked: onClicked:
{
if (!model.package.isMissingPackageInformation)
{ {
packages.selectedPackage = model.package; packages.selectedPackage = model.package;
contextStack.push(packageDetailsComponent); contextStack.push(packageDetailsComponent);
} }
}
PackageCard PackageCard
{ {

View file

@ -14,7 +14,7 @@ UM.SimpleButton
width: UM.Theme.getSize("save_button_specs_icons").width width: UM.Theme.getSize("save_button_specs_icons").width
height: UM.Theme.getSize("save_button_specs_icons").height height: UM.Theme.getSize("save_button_specs_icons").height
iconSource: "model_checker.svg" iconSource: Qt.resolvedUrl("model_checker.svg")
anchors.verticalCenter: parent ? parent.verticalCenter : undefined anchors.verticalCenter: parent ? parent.verticalCenter : undefined
color: UM.Theme.getColor("text_scene") color: UM.Theme.getColor("text_scene")
hoverColor: UM.Theme.getColor("text_scene_hover") hoverColor: UM.Theme.getColor("text_scene_hover")

View file

@ -122,6 +122,7 @@ Rectangle
} }
visible: !isNetworkConfigured && isNetworkConfigurable visible: !isNetworkConfigured && isNetworkConfigurable
width: childrenRect.width width: childrenRect.width
height: childrenRect.height
UM.ColorImage UM.ColorImage
{ {
@ -132,7 +133,7 @@ Rectangle
width: UM.Theme.getSize("icon_indicator").width width: UM.Theme.getSize("icon_indicator").width
height: UM.Theme.getSize("icon_indicator").height height: UM.Theme.getSize("icon_indicator").height
} }
Label UM.Label
{ {
id: manageQueueText id: manageQueueText
anchors anchors
@ -144,7 +145,6 @@ Rectangle
color: UM.Theme.getColor("text_link") color: UM.Theme.getColor("text_link")
font: UM.Theme.getFont("medium") font: UM.Theme.getFont("medium")
text: catalog.i18nc("@label link to technical assistance", "View user manuals online") text: catalog.i18nc("@label link to technical assistance", "View user manuals online")
renderType: Text.NativeRendering
} }
MouseArea MouseArea
{ {
@ -155,14 +155,13 @@ Rectangle
onExited: manageQueueText.font.underline = false onExited: manageQueueText.font.underline = false
} }
} }
Label UM.Label
{ {
id: noConnectionLabel id: noConnectionLabel
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
visible: !isNetworkConfigurable visible: !isNetworkConfigurable
text: catalog.i18nc("@info", "In order to monitor your print from Cura, please connect the printer.") text: catalog.i18nc("@info", "In order to monitor your print from Cura, please connect the printer.")
font: UM.Theme.getFont("medium") font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
width: contentWidth width: contentWidth
} }

View file

@ -232,7 +232,7 @@ UM.Dialog
} }
onObjectAdded: function(index, object) { scriptsMenu.insertItem(index, object)} onObjectAdded: function(index, object) { scriptsMenu.insertItem(index, object)}
onObjectRemoved: function(object) { scriptsMenu.removeItem(object) } onObjectRemoved: function(index, object) { scriptsMenu.removeItem(object) }
} }
} }
@ -245,7 +245,7 @@ UM.Dialog
height: parent.height height: parent.height
id: settingsPanel id: settingsPanel
Label UM.Label
{ {
id: scriptSpecsHeader id: scriptSpecsHeader
text: manager.selectedScriptIndex == -1 ? catalog.i18nc("@label", "Settings") : base.activeScriptName text: manager.selectedScriptIndex == -1 ? catalog.i18nc("@label", "Settings") : base.activeScriptName
@ -262,7 +262,6 @@ UM.Dialog
elide: Text.ElideRight elide: Text.ElideRight
height: 20 * screenScaleFactor height: 20 * screenScaleFactor
font: UM.Theme.getFont("large_bold") font: UM.Theme.getFont("large_bold")
color: UM.Theme.getColor("text")
} }
ListView ListView
@ -475,7 +474,7 @@ UM.Dialog
} }
toolTipContentAlignment: UM.Enums.ContentAlignment.AlignLeft toolTipContentAlignment: UM.Enums.ContentAlignment.AlignLeft
onClicked: dialog.show() onClicked: dialog.show()
// iconSource: "Script.svg" iconSource: Qt.resolvedUrl("Script.svg")
fixedWidthMode: false fixedWidthMode: false
} }

View file

@ -330,7 +330,7 @@ class PauseAtHeight(Script):
current_height = current_z - layer_0_z current_height = current_z - layer_0_z
if current_height < pause_height: if current_height < pause_height:
continue # Scan the enitre layer, z-changes are not always on the same/first line. continue # Scan the entire layer, z-changes are not always on the same/first line.
# Pause at layer # Pause at layer
else: else:

View file

@ -106,26 +106,34 @@ Item
{ {
id: openProviderColumn id: openProviderColumn
//The column doesn't automatically listen to its children rect if the children change internally, so we need to explicitly update the size. // Automatically set the width to fit the widest MenuItem
onChildrenRectChanged: // Based on https://martin.rpdev.net/2018/03/13/qt-quick-controls-2-automatically-set-the-width-of-menus.html
function setWidth()
{ {
popup.implicitHeight = childrenRect.height var result = 0;
popup.implicitWidth = childrenRect.width var padding = 0;
} for (var i = 0; i < fileProviderRepeater.count; ++i) {
onPositioningComplete: var item = fileProviderRepeater.itemAt(i);
if (item.hasOwnProperty("implicitWidth"))
{ {
popup.implicitHeight = childrenRect.height var itemWidth = item.implicitWidth;
popup.implicitWidth = childrenRect.width result = Math.max(itemWidth, result);
padding = Math.max(item.padding, padding);
} }
}
return result + padding * 2;
}
width: setWidth()
Repeater Repeater
{ {
id: fileProviderRepeater
model: prepareMenu.fileProviderModel model: prepareMenu.fileProviderModel
delegate: Button delegate: Button
{ {
leftPadding: UM.Theme.getSize("default_margin").width leftPadding: UM.Theme.getSize("default_margin").width
rightPadding: UM.Theme.getSize("default_margin").width rightPadding: UM.Theme.getSize("default_margin").width
width: contentItem.width + leftPadding + rightPadding width: openProviderColumn.width
height: UM.Theme.getSize("action_button").height height: UM.Theme.getSize("action_button").height
hoverEnabled: true hoverEnabled: true

View file

@ -80,6 +80,18 @@ class RemovableDriveOutputDevice(OutputDevice):
if extension: # Not empty string. if extension: # Not empty string.
extension = "." + extension extension = "." + extension
file_name = os.path.join(self.getId(), file_name + extension) file_name = os.path.join(self.getId(), file_name + extension)
self._performWrite(file_name, preferred_format, writer, nodes)
def _performWrite(self, file_name, preferred_format, writer, nodes):
"""Writes the specified nodes to the removable drive. This is split from
requestWrite to allow interception in other plugins. See Ultimaker/Cura#10917.
:param file_name: File path to write to.
:param preferred_format: Preferred file format to write to.
:param writer: Writer for writing to the file.
:param nodes: A collection of scene nodes that should be written to the
file.
"""
try: try:
Logger.log("d", "Writing to %s", file_name) Logger.log("d", "Writing to %s", file_name)

View file

@ -21,7 +21,7 @@ class RemovableDrivePlugin(OutputDevicePlugin):
super().__init__() super().__init__()
self._update_thread = threading.Thread(target = self._updateThread) self._update_thread = threading.Thread(target = self._updateThread)
self._update_thread.deamon = True self._update_thread.daemon = True
self._check_updates = True self._check_updates = True

View file

@ -286,9 +286,7 @@ Cura.ExpandableComponent
UM.Label UM.Label
{ {
text: label text: label
font: UM.Theme.getFont("default")
elide: Text.ElideRight elide: Text.ElideRight
renderType: Text.NativeRendering
color: UM.Theme.getColor("setting_control_text") color: UM.Theme.getColor("setting_control_text")
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: legendModelCheckBox.left anchors.left: legendModelCheckBox.left

View file

@ -61,7 +61,7 @@ Window
right: parent.right right: parent.right
} }
Label UM.Label
{ {
id: headerText id: headerText
anchors anchors
@ -71,9 +71,7 @@ Window
right: parent.right right: parent.right
} }
text: catalog.i18nc("@text:window", "Ultimaker Cura collects anonymous data in order to improve the print quality and user experience. Below is an example of all the data that is shared:") text: catalog.i18nc("@text:window", "Ultimaker Cura collects anonymous data in order to improve the print quality and user experience. Below is an example of all the data that is shared:")
color: UM.Theme.getColor("text")
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
renderType: Text.NativeRendering
} }
Cura.ScrollableTextArea Cura.ScrollableTextArea

View file

@ -295,7 +295,6 @@ Cura.MachineAction
UM.Label UM.Label
{ {
text: catalog.i18nc("@label", "Enter the IP address of your printer on the network.") text: catalog.i18nc("@label", "Enter the IP address of your printer on the network.")
renderType: Text.NativeRendering
} }
Cura.TextField Cura.TextField

View file

@ -3,7 +3,7 @@
import QtQuick 2.2 import QtQuick 2.2
import QtQuick.Controls 2.0 import QtQuick.Controls 2.0
import UM 1.3 as UM import UM 1.5 as UM
import Cura 1.6 as Cura import Cura 1.6 as Cura
@ -57,32 +57,25 @@ Item
width: Math.max(materialLabel.contentWidth, 60 * screenScaleFactor) // TODO: Theme! width: Math.max(materialLabel.contentWidth, 60 * screenScaleFactor) // TODO: Theme!
radius: 2 * screenScaleFactor // TODO: Theme! radius: 2 * screenScaleFactor // TODO: Theme!
Label UM.Label
{ {
id: materialLabel id: materialLabel
anchors.top: parent.top anchors.top: parent.top
color: UM.Theme.getColor("text")
elide: Text.ElideRight elide: Text.ElideRight
font: UM.Theme.getFont("default") // 12pt, regular
text: "" text: ""
visible: text !== "" visible: text !== ""
renderType: Text.NativeRendering
} }
Label UM.Label
{ {
id: printCoreLabel id: printCoreLabel
anchors.top: materialLabel.bottom anchors.top: materialLabel.bottom
color: UM.Theme.getColor("text")
elide: Text.ElideRight elide: Text.ElideRight
font: UM.Theme.getFont("default_bold") // 12pt, bold font: UM.Theme.getFont("default_bold") // 12pt, bold
text: "" text: ""
visible: text !== "" visible: text !== ""
renderType: Text.NativeRendering
} }
} }
} }

View file

@ -22,7 +22,7 @@ Item
property int size: 32 * screenScaleFactor // TODO: Theme! property int size: 32 * screenScaleFactor // TODO: Theme!
// THe extruder icon source; NOTE: This shouldn't need to be changed // THe extruder icon source; NOTE: This shouldn't need to be changed
property string iconSource: "../svg/icons/Extruder.svg" property string iconSource: Qt.resolvedUrl("../svg/icons/Extruder.svg")
height: size height: size
width: size width: size

View file

@ -3,7 +3,7 @@
import QtQuick 2.3 import QtQuick 2.3
import QtQuick.Controls 2.0 import QtQuick.Controls 2.0
import UM 1.3 as UM import UM 1.5 as UM
/** /**
* A MonitorInfoBlurb is an extension of the GenericPopUp used to show static information (vs. interactive context * A MonitorInfoBlurb is an extension of the GenericPopUp used to show static information (vs. interactive context
@ -31,7 +31,7 @@ Item
id: contentWrapper id: contentWrapper
implicitWidth: childrenRect.width implicitWidth: childrenRect.width
implicitHeight: innerLabel.contentHeight + 2 * innerLabel.padding implicitHeight: innerLabel.contentHeight + 2 * innerLabel.padding
Label UM.Label
{ {
id: innerLabel id: innerLabel
padding: 12 * screenScaleFactor // TODO: Theme! padding: 12 * screenScaleFactor // TODO: Theme!
@ -39,7 +39,6 @@ Item
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
width: 240 * screenScaleFactor // TODO: Theme! width: 240 * screenScaleFactor // TODO: Theme!
color: UM.Theme.getColor("monitor_tooltip_text") color: UM.Theme.getColor("monitor_tooltip_text")
font: UM.Theme.getFont("default")
} }
} }
} }

View file

@ -45,7 +45,7 @@ Item
anchors.centerIn: printJobPreview anchors.centerIn: printJobPreview
color: UM.Theme.getColor("monitor_placeholder_image") color: UM.Theme.getColor("monitor_placeholder_image")
height: printJobPreview.height height: printJobPreview.height
source: "../svg/ultibot.svg" source: Qt.resolvedUrl("../svg/ultibot.svg")
/* Since print jobs ALWAYS have an image url, we have to check if that image URL errors or /* Since print jobs ALWAYS have an image url, we have to check if that image URL errors or
not in order to determine if we show the placeholder (ultibot) image instead. */ not in order to determine if we show the placeholder (ultibot) image instead. */
visible: printJob && previewImage.status == Image.Error visible: printJob && previewImage.status == Image.Error

View file

@ -252,7 +252,7 @@ Item
bottom: parent.bottom bottom: parent.bottom
bottomMargin: 20 * screenScaleFactor // TODO: Theme! bottomMargin: 20 * screenScaleFactor // TODO: Theme!
} }
iconSource: "../svg/icons/CameraPhoto.svg" iconSource: Qt.resolvedUrl("../svg/icons/CameraPhoto.svg")
enabled: !cloudConnection enabled: !cloudConnection
visible: printer visible: printer
} }

View file

@ -4,7 +4,7 @@ import QtQuick 2.2
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import UM 1.1 as UM import UM 1.5 as UM
UM.Dialog { UM.Dialog {
@ -70,7 +70,7 @@ UM.Dialog {
name: "cura"; name: "cura";
} }
Label { UM.Label {
id: manualPrinterSelectionLabel; id: manualPrinterSelectionLabel;
anchors { anchors {
left: parent.left; left: parent.left;
@ -79,8 +79,6 @@ UM.Dialog {
} }
height: 20 * screenScaleFactor; height: 20 * screenScaleFactor;
text: catalog.i18nc("@label", "Printer selection"); text: catalog.i18nc("@label", "Printer selection");
wrapMode: Text.Wrap;
renderType: Text.NativeRendering;
} }
ComboBox { ComboBox {

View file

@ -236,6 +236,8 @@ class CloudOutputDeviceManager:
) )
message.show() message.show()
new_devices_added = []
for idx, device in enumerate(new_devices): for idx, device in enumerate(new_devices):
message_text = self.i18n_catalog.i18nc("info:status Filled in with printer name and printer model.", "Adding printer {name} ({model}) from your account").format(name = device.name, model = device.printerTypeName) message_text = self.i18n_catalog.i18nc("info:status Filled in with printer name and printer model.", "Adding printer {name} ({model}) from your account").format(name = device.name, model = device.printerTypeName)
message.setText(message_text) message.setText(message_text)
@ -246,21 +248,25 @@ class CloudOutputDeviceManager:
# If there is no active machine, activate the first available cloud printer # If there is no active machine, activate the first available cloud printer
activate = not CuraApplication.getInstance().getMachineManager().activeMachine activate = not CuraApplication.getInstance().getMachineManager().activeMachine
self._createMachineFromDiscoveredDevice(device.getId(), activate = activate)
if self._createMachineFromDiscoveredDevice(device.getId(), activate = activate):
new_devices_added.append(device)
message.setProgress(None) message.setProgress(None)
max_disp_devices = 3 max_disp_devices = 3
if len(new_devices) > max_disp_devices: if len(new_devices_added) > max_disp_devices:
num_hidden = len(new_devices) - max_disp_devices num_hidden = len(new_devices_added) - max_disp_devices
device_name_list = ["<li>{} ({})</li>".format(device.name, device.printerTypeName) for device in new_devices[0:max_disp_devices]] device_name_list = ["<li>{} ({})</li>".format(device.name, device.printerTypeName) for device in new_devices[0:max_disp_devices]]
device_name_list.append("<li>" + self.i18n_catalog.i18ncp("info:{0} gets replaced by a number of printers", "... and {0} other", "... and {0} others", num_hidden) + "</li>") device_name_list.append("<li>" + self.i18n_catalog.i18ncp("info:{0} gets replaced by a number of printers", "... and {0} other", "... and {0} others", num_hidden) + "</li>")
device_names = "".join(device_name_list) device_names = "".join(device_name_list)
else: else:
device_names = "".join(["<li>{} ({})</li>".format(device.name, device.printerTypeName) for device in new_devices]) device_names = "".join(["<li>{} ({})</li>".format(device.name, device.printerTypeName) for device in new_devices_added])
if new_devices_added:
message_text = self.i18n_catalog.i18nc("info:status", "Printers added from Digital Factory:") + "<ul>" + device_names + "</ul>" message_text = self.i18n_catalog.i18nc("info:status", "Printers added from Digital Factory:") + "<ul>" + device_names + "</ul>"
message.setText(message_text) message.setText(message_text)
else:
message.hide()
def _updateOnlinePrinters(self, printer_responses: Dict[str, CloudClusterResponse]) -> None: def _updateOnlinePrinters(self, printer_responses: Dict[str, CloudClusterResponse]) -> None:
""" """
@ -385,23 +391,25 @@ class CloudOutputDeviceManager:
if device.key in output_device_manager.getOutputDeviceIds(): if device.key in output_device_manager.getOutputDeviceIds():
output_device_manager.removeOutputDevice(device.key) output_device_manager.removeOutputDevice(device.key)
def _createMachineFromDiscoveredDevice(self, key: str, activate: bool = True) -> None: def _createMachineFromDiscoveredDevice(self, key: str, activate: bool = True) -> bool:
device = self._remote_clusters[key] device = self._remote_clusters[key]
if not device: if not device:
return return False
# Create a new machine. # Create a new machine.
# We do not use use MachineManager.addMachine here because we need to set the cluster ID before activating it. # We do not use use MachineManager.addMachine here because we need to set the cluster ID before activating it.
new_machine = CuraStackBuilder.createMachine(device.name, device.printerType) new_machine = CuraStackBuilder.createMachine(device.name, device.printerType, show_warning_message=False)
if not new_machine: if not new_machine:
Logger.log("e", "Failed creating a new machine") Logger.log("e", "Failed creating a new machine")
return return False
self._setOutputDeviceMetadata(device, new_machine) self._setOutputDeviceMetadata(device, new_machine)
if activate: if activate:
CuraApplication.getInstance().getMachineManager().setActiveMachine(new_machine.getId()) CuraApplication.getInstance().getMachineManager().setActiveMachine(new_machine.getId())
return True
def _connectToActiveMachine(self) -> None: def _connectToActiveMachine(self) -> None:
"""Callback for when the active machine was changed by the user""" """Callback for when the active machine was changed by the user"""
@ -480,7 +488,7 @@ class CloudOutputDeviceManager:
if remove_printers_ids == all_ids: if remove_printers_ids == all_ids:
question_content = self.i18n_catalog.i18nc("@label", "You are about to remove all printers from Cura. This action cannot be undone.\nAre you sure you want to continue?") question_content = self.i18n_catalog.i18nc("@label", "You are about to remove all printers from Cura. This action cannot be undone.\nAre you sure you want to continue?")
result = QMessageBox.question(None, question_title, question_content) result = QMessageBox.question(None, question_title, question_content)
if result == QMessageBox.ButtonRole.NoRole: if result == QMessageBox.StandardButton.No:
return return
for machine_cloud_id in self.reported_device_ids: for machine_cloud_id in self.reported_device_ids:

View file

@ -169,7 +169,10 @@ class ClusterApiClient:
""" """
def parse() -> None: def parse() -> None:
try:
self._anti_gc_callbacks.remove(parse) self._anti_gc_callbacks.remove(parse)
except ValueError: # Already removed asynchronously.
return # Then the rest of the function is also already executed.
# Don't try to parse the reply if we didn't get one # Don't try to parse the reply if we didn't get one
if reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) is None: if reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) is None:

View file

@ -5,7 +5,7 @@ import QtQuick 2.10
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import UM 1.3 as UM import UM 1.5 as UM
import Cura 1.1 as Cura import Cura 1.1 as Cura
@ -23,18 +23,16 @@ Cura.MachineAction
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
width: parent.width * 3 / 4 width: parent.width * 3 / 4
Label UM.Label
{ {
id: pageTitle id: pageTitle
width: parent.width width: parent.width
text: catalog.i18nc("@title", "Build Plate Leveling") text: catalog.i18nc("@title", "Build Plate Leveling")
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
font: UM.Theme.getFont("medium") font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
} }
Label UM.Label
{ {
id: pageDescription id: pageDescription
anchors.top: pageTitle.bottom anchors.top: pageTitle.bottom
@ -42,12 +40,9 @@ Cura.MachineAction
width: parent.width width: parent.width
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "To make sure your prints will come out great, you can now adjust your buildplate. When you click 'Move to Next Position' the nozzle will move to the different positions that can be adjusted.") text: catalog.i18nc("@label", "To make sure your prints will come out great, you can now adjust your buildplate. When you click 'Move to Next Position' the nozzle will move to the different positions that can be adjusted.")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
} }
Label UM.Label
{ {
id: bedlevelingText id: bedlevelingText
anchors.top: pageDescription.bottom anchors.top: pageDescription.bottom
@ -55,9 +50,6 @@ Cura.MachineAction
width: parent.width width: parent.width
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "For every position; insert a piece of paper under the nozzle and adjust the print build plate height. The print build plate height is right when the paper is slightly gripped by the tip of the nozzle.") text: catalog.i18nc("@label", "For every position; insert a piece of paper under the nozzle and adjust the print build plate height. The print build plate height is right when the paper is slightly gripped by the tip of the nozzle.")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
} }
Row Row

View file

@ -20,7 +20,7 @@ Cura.MachineAction
anchors.topMargin: UM.Theme.getSize("default_margin").width * 5 anchors.topMargin: UM.Theme.getSize("default_margin").width * 5
anchors.leftMargin: UM.Theme.getSize("default_margin").width * 4 anchors.leftMargin: UM.Theme.getSize("default_margin").width * 4
Label UM.Label
{ {
id: pageDescription id: pageDescription
anchors.top: parent.top anchors.top: parent.top
@ -29,8 +29,6 @@ Cura.MachineAction
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Please select any upgrades made to this Ultimaker Original") text: catalog.i18nc("@label","Please select any upgrades made to this Ultimaker Original")
font: UM.Theme.getFont("medium") font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
} }
UM.CheckBox UM.CheckBox

View file

@ -343,6 +343,9 @@ class XmlMaterialProfile(InstanceContainer):
return stream.getvalue().decode("utf-8") return stream.getvalue().decode("utf-8")
def getFileName(self) -> str:
return (self.getMetaDataEntry("base_file") + ".xml.fdm_material").replace(" ", "+")
# Recursively resolve loading inherited files # Recursively resolve loading inherited files
def _resolveInheritance(self, file_name): def _resolveInheritance(self, file_name):
xml = self._loadFile(file_name) xml = self._loadFile(file_name)
@ -477,6 +480,15 @@ class XmlMaterialProfile(InstanceContainer):
return version * 1000000 + setting_version return version * 1000000 + setting_version
@classmethod
def getMetadataFromSerialized(cls, serialized: str, property_name: str) -> str:
data = ET.fromstring(serialized)
metadata = data.find("./um:metadata", cls.__namespaces)
property = metadata.find("./um:" + property_name, cls.__namespaces)
# This is a necessary property != None check, xml library overrides __bool__ to return False in cases when Element is not None.
return property.text if property != None else ""
def deserialize(self, serialized, file_name = None): def deserialize(self, serialized, file_name = None):
"""Overridden from InstanceContainer""" """Overridden from InstanceContainer"""

View file

@ -162,10 +162,6 @@
{ {
"default_value": true "default_value": true
}, },
"jerk_print":
{
"default_value": 12
},
"jerk_travel": "jerk_travel":
{ {
"value": "jerk_print if magic_spiralize else 20" "value": "jerk_print if magic_spiralize else 20"

View file

@ -44,7 +44,6 @@
"machine_max_jerk_xy": { "value": 8 }, "machine_max_jerk_xy": { "value": 8 },
"machine_max_jerk_z": { "value": 0.4 }, "machine_max_jerk_z": { "value": 0.4 },
"machine_max_jerk_e": { "value": 5 }, "machine_max_jerk_e": { "value": 5 },
"machine_heated_bed": { "default_value": true },
"material_diameter": { "default_value": 1.75 }, "material_diameter": { "default_value": 1.75 },
"infill_overlap": { "default_value": 15 }, "infill_overlap": { "default_value": 15 },
"acceleration_print": { "value": 500 }, "acceleration_print": { "value": 500 },

View file

@ -87,7 +87,6 @@
"wall_0_wipe_dist": { "value": 0.2 }, "wall_0_wipe_dist": { "value": 0.2 },
"fill_outline_gaps": { "value": false }, "fill_outline_gaps": { "value": false },
"filter_out_tiny_gaps": { "value": true },
"retraction_speed": { "retraction_speed": {
"maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')", "maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')",

View file

@ -192,7 +192,6 @@
"wall_0_wipe_dist": { "value": 0.0 }, "wall_0_wipe_dist": { "value": 0.0 },
"fill_outline_gaps": { "value": false }, "fill_outline_gaps": { "value": false },
"filter_out_tiny_gaps": { "value": false },
"retraction_speed": { "retraction_speed": {
"maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')", "maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')",

View file

@ -97,7 +97,6 @@
"wall_0_wipe_dist": { "value": 0.0 }, "wall_0_wipe_dist": { "value": 0.0 },
"fill_outline_gaps": { "value": false }, "fill_outline_gaps": { "value": false },
"filter_out_tiny_gaps": { "value": false },
"retraction_speed": { "retraction_speed": {
"maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')", "maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')",

View file

@ -27,7 +27,6 @@
"infill_overlap": { "value": 15.0 }, "infill_overlap": { "value": 15.0 },
"skin_overlap": { "value": 20.0 }, "skin_overlap": { "value": 20.0 },
"fill_outline_gaps": { "value": true }, "fill_outline_gaps": { "value": true },
"filter_out_tiny_gaps": { "value": true },
"roofing_layer_count": { "value": 2 }, "roofing_layer_count": { "value": 2 },
"xy_offset_layer_0": { "value": -0.1 }, "xy_offset_layer_0": { "value": -0.1 },
"speed_print": { "value": 50 }, "speed_print": { "value": 50 },

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