mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-18 04:07:57 -06:00
Merge branch 'main' into fix_postprocessing_script_folders
This commit is contained in:
commit
378e5fedea
848 changed files with 114251 additions and 99019 deletions
4
.github/ISSUE_TEMPLATE/bugreport.yaml
vendored
4
.github/ISSUE_TEMPLATE/bugreport.yaml
vendored
|
@ -1,6 +1,6 @@
|
|||
name: Bug Report
|
||||
description: Create a report to help us fix issues.
|
||||
labels: "Type: Bug"
|
||||
labels: ["Type: Bug", "Status: Triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
@ -14,7 +14,7 @@ body:
|
|||
attributes:
|
||||
label: Application Version
|
||||
description: The version of Cura this issue occurs with.
|
||||
placeholder: 4.9.0
|
||||
placeholder: 5.0.0
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
|
13
.github/no-response.yml
vendored
13
.github/no-response.yml
vendored
|
@ -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.
|
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
|
@ -8,6 +8,9 @@ on:
|
|||
- '4.*'
|
||||
- 'CURA-*'
|
||||
pull_request:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
215
.github/workflows/cura-installer.yml
vendored
Normal file
215
.github/workflows/cura-installer.yml
vendored
Normal 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
31
.github/workflows/no-response.yml
vendored
Normal 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.
|
2
.github/workflows/requirements-conan-package.txt
vendored
Normal file
2
.github/workflows/requirements-conan-package.txt
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
conan
|
||||
sip==6.5.1
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -38,6 +38,7 @@ cura.desktop
|
|||
.settings
|
||||
|
||||
#Externally located plug-ins commonly installed by our devs.
|
||||
plugins/BarbarianPlugin
|
||||
plugins/cura-big-flame-graph
|
||||
plugins/cura-camera-position
|
||||
plugins/cura-god-mode-plugin
|
||||
|
@ -64,6 +65,7 @@ plugins/CuraRemoteSupport
|
|||
plugins/ModelCutter
|
||||
plugins/PrintProfileCreator
|
||||
plugins/MultiPrintPlugin
|
||||
plugins/CuraOrientationPlugin
|
||||
|
||||
#Build stuff
|
||||
CMakeCache.txt
|
||||
|
|
33
CITATION.cff
33
CITATION.cff
|
@ -1,11 +1,30 @@
|
|||
# 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:
|
||||
cff-version: "1.1.0"
|
||||
date-released: 2021-06-28
|
||||
license: "LGPL-3.0"
|
||||
message: "If you use this software, please cite it using these metadata."
|
||||
repository-code: "https://github.com/ultimaker/cura/"
|
||||
title: "Ultimaker Cura"
|
||||
version: "4.12.0"
|
||||
- name: Ultimaker B.V.
|
||||
contact:
|
||||
- email: info@ultimaker.com
|
||||
name: "Ultimaker B.V."
|
||||
url: 'https://ultimaker.com/software/ultimaker-cura'
|
||||
repository-code: 'https://github.com/Ultimaker/Cura'
|
||||
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
|
||||
...
|
||||
|
|
|
@ -25,9 +25,10 @@
|
|||
</description>
|
||||
<screenshots>
|
||||
<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>
|
||||
</screenshots>
|
||||
<url type="homepage">https://ultimaker.com/software/ultimaker-cura?utm_source=cura&utm_medium=software&utm_campaign=cura-update-linux</url>
|
||||
<translation type="gettext">Cura</translation>
|
||||
<content_rating type="oars-1.1" />
|
||||
</component>
|
||||
|
|
|
@ -565,8 +565,8 @@ class BuildVolume(SceneNode):
|
|||
self._updateScaleFactor()
|
||||
|
||||
self._volume_aabb = AxisAlignedBox(
|
||||
minimum = Vector(min_w, min_h - 1.0, min_d).scale(self._scale_vector),
|
||||
maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_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)
|
||||
)
|
||||
|
||||
bed_adhesion_size = self.getEdgeDisallowedSize()
|
||||
|
@ -575,8 +575,8 @@ class BuildVolume(SceneNode):
|
|||
# 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.
|
||||
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),
|
||||
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)
|
||||
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)
|
||||
)
|
||||
|
||||
self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds # type: ignore
|
||||
|
@ -645,7 +645,7 @@ class BuildVolume(SceneNode):
|
|||
for extruder in extruders:
|
||||
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")
|
||||
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)
|
||||
|
@ -656,7 +656,7 @@ class BuildVolume(SceneNode):
|
|||
else:
|
||||
self._height = self._global_container_stack.getProperty("machine_height", "value")
|
||||
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._updateDisallowedAreas()
|
||||
|
@ -752,8 +752,8 @@ class BuildVolume(SceneNode):
|
|||
return
|
||||
self._updateScaleFactor()
|
||||
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._depth = self._global_container_stack.getProperty("machine_depth", "value") * self._scale_vector.y
|
||||
self._width = self._global_container_stack.getProperty("machine_width", "value")
|
||||
self._depth = self._global_container_stack.getProperty("machine_depth", "value")
|
||||
self._shape = self._global_container_stack.getProperty("machine_shape", "value")
|
||||
|
||||
def _updateDisallowedAreasAndRebuild(self):
|
||||
|
@ -770,14 +770,6 @@ class BuildVolume(SceneNode):
|
|||
self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks())
|
||||
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:
|
||||
if not self._global_container_stack:
|
||||
return
|
||||
|
@ -833,11 +825,9 @@ class BuildVolume(SceneNode):
|
|||
|
||||
self._disallowed_areas = []
|
||||
for extruder_id in result_areas:
|
||||
self._scaleAreas(result_areas[extruder_id])
|
||||
self._disallowed_areas.extend(result_areas[extruder_id])
|
||||
self._disallowed_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])
|
||||
|
||||
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_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 border_size - left_unreachable_border > 0:
|
||||
result[extruder_id].append(Polygon(numpy.array([
|
||||
|
|
|
@ -261,7 +261,7 @@ class CrashHandler:
|
|||
opengl_instance = OpenGL.getInstance()
|
||||
if not opengl_instance:
|
||||
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 += 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:
|
||||
with configure_scope() as scope:
|
||||
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_type", opengl_instance.getGPUType())
|
||||
scope.set_tag("active_machine", active_machine_definition_id)
|
||||
|
|
|
@ -115,6 +115,8 @@ from . import CuraActions
|
|||
from . import PlatformPhysics
|
||||
from . import PrintJobPreviewImageProvider
|
||||
from .AutoSave import AutoSave
|
||||
from .Machines.Models.ActiveIntentQualitiesModel import ActiveIntentQualitiesModel
|
||||
from .Machines.Models.IntentSelectionModel import IntentSelectionModel
|
||||
from .SingleInstance import SingleInstance
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -257,6 +259,7 @@ class CuraApplication(QtApplication):
|
|||
|
||||
from UM.CentralFileStorage import CentralFileStorage
|
||||
CentralFileStorage.setIsEnterprise(ApplicationMetadata.IsEnterpriseVersion)
|
||||
Resources.setIsEnterprise(ApplicationMetadata.IsEnterpriseVersion)
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def ultimakerCloudApiRootUrl(self) -> str:
|
||||
|
@ -315,7 +318,7 @@ class CuraApplication(QtApplication):
|
|||
def initialize(self) -> None:
|
||||
self.__addExpectedResourceDirsAndSearchPaths() # Must be added before init of super
|
||||
|
||||
super().initialize()
|
||||
super().initialize(ApplicationMetadata.IsEnterpriseVersion)
|
||||
|
||||
self._preferences.addPreference("cura/single_instance", False)
|
||||
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)
|
||||
|
||||
app_root = os.path.abspath(os.path.join(os.path.dirname(sys.executable)))
|
||||
Resources.addSearchPath(os.path.join(app_root, "share", "cura", "resources"))
|
||||
Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "share", "cura", "resources"))
|
||||
Resources.addSecureSearchPath(os.path.join(app_root, "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"):
|
||||
resource_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources")
|
||||
Resources.addSearchPath(resource_path)
|
||||
Resources.addSecureSearchPath(resource_path)
|
||||
|
||||
@classmethod
|
||||
def _initializeSettingDefinitions(cls):
|
||||
|
@ -943,6 +945,7 @@ class CuraApplication(QtApplication):
|
|||
self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles))
|
||||
self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing engine..."))
|
||||
self.initializeEngine()
|
||||
self.getTheme().setCheckIfTrusted(ApplicationMetadata.IsEnterpriseVersion)
|
||||
|
||||
# Initialize UI state
|
||||
controller.setActiveStage("PrepareStage")
|
||||
|
@ -1191,6 +1194,8 @@ class CuraApplication(QtApplication):
|
|||
qmlRegisterType(NozzleModel, "Cura", 1, 0, "NozzleModel")
|
||||
qmlRegisterType(IntentModel, "Cura", 1, 6, "IntentModel")
|
||||
qmlRegisterType(IntentCategoryModel, "Cura", 1, 6, "IntentCategoryModel")
|
||||
qmlRegisterType(IntentSelectionModel, "Cura", 1, 7, "IntentSelectionModel")
|
||||
qmlRegisterType(ActiveIntentQualitiesModel, "Cura", 1, 7, "ActiveIntentQualitiesModel")
|
||||
|
||||
self.processEvents()
|
||||
qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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 UM.Logger import Logger
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from cura.CuraApplication import CuraApplication # To find some resource types.
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
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.i18n import i18nCatalog
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -51,6 +57,49 @@ class CuraPackageManager(PackageManager):
|
|||
|
||||
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]]]:
|
||||
"""Returns a list of where the package is used
|
||||
|
||||
|
|
|
@ -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.add(key)
|
||||
self._setResult(True, keys_to_recheck = keys_to_recheck)
|
||||
continue
|
||||
return
|
||||
|
||||
# Schedule the check for the next key
|
||||
self._application.callLater(self._checkStack)
|
||||
|
|
131
cura/Machines/Models/ActiveIntentQualitiesModel.py
Normal file
131
cura/Machines/Models/ActiveIntentQualitiesModel.py
Normal 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
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ class GlobalStacksModel(ListModel):
|
|||
continue
|
||||
|
||||
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)
|
||||
|
||||
default_removal_warning = self._catalog.i18nc(
|
||||
|
|
|
@ -111,7 +111,7 @@ class IntentCategoryModel(ListModel):
|
|||
except ValueError:
|
||||
weight = 99
|
||||
result.append({
|
||||
"name": IntentCategoryModel.translation(category, "name", category),
|
||||
"name": IntentCategoryModel.translation(category, "name", category.title()),
|
||||
"description": IntentCategoryModel.translation(category, "description", None),
|
||||
"intent_category": category,
|
||||
"weight": weight,
|
||||
|
|
129
cura/Machines/Models/IntentSelectionModel.py
Normal file
129
cura/Machines/Models/IntentSelectionModel.py
Normal 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)
|
||||
|
||||
|
|
@ -34,62 +34,6 @@ class MaterialManagementModel(QObject):
|
|||
def __init__(self, parent: Optional[QObject] = None) -> None:
|
||||
super().__init__(parent = parent)
|
||||
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)
|
||||
def canMaterialBeRemoved(self, material_node: "MaterialNode") -> bool:
|
||||
|
|
|
@ -358,8 +358,9 @@ class QualityManagementModel(ListModel):
|
|||
"quality_type": quality_type,
|
||||
"quality_changes_group": None,
|
||||
"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
|
||||
intent_translations_list = list(intent_translations)
|
||||
|
||||
|
|
|
@ -119,16 +119,16 @@ class PrintJobOutputModel(QObject):
|
|||
|
||||
@pyqtProperty(int, notify = timeTotalChanged)
|
||||
def timeTotal(self) -> int:
|
||||
return self._time_total
|
||||
return int(self._time_total)
|
||||
|
||||
@pyqtProperty(int, notify = timeElapsedChanged)
|
||||
def timeElapsed(self) -> int:
|
||||
return self._time_elapsed
|
||||
return int(self._time_elapsed)
|
||||
|
||||
@pyqtProperty(int, notify = timeElapsedChanged)
|
||||
def timeRemaining(self) -> int:
|
||||
# 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)
|
||||
def progress(self) -> float:
|
||||
|
|
|
@ -114,7 +114,7 @@ class ContainerManager(QObject):
|
|||
for _ in range(len(entries)):
|
||||
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
|
||||
item[entry_name] = entry_value
|
||||
|
||||
|
@ -206,7 +206,7 @@ class ContainerManager(QObject):
|
|||
if os.path.exists(file_url):
|
||||
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))
|
||||
if result == QMessageBox.ButtonRole.NoRole:
|
||||
if result == QMessageBox.StandardButton.No:
|
||||
return {"status": "cancelled", "message": "User cancelled"}
|
||||
|
||||
try:
|
||||
|
|
|
@ -139,7 +139,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
if os.path.exists(file_name):
|
||||
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))
|
||||
if result == QMessageBox.ButtonRole.NoRole:
|
||||
if result == QMessageBox.StandardButton.No:
|
||||
return False
|
||||
|
||||
profile_writer = self._findProfileWriter(extension, description)
|
||||
|
|
|
@ -17,7 +17,7 @@ class CuraStackBuilder:
|
|||
"""Contains helper functions to create new machines."""
|
||||
|
||||
@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.
|
||||
|
||||
:param name: The name of the new machine.
|
||||
|
@ -34,6 +34,7 @@ class CuraStackBuilder:
|
|||
|
||||
definitions = registry.findDefinitionContainers(id = definition_id)
|
||||
if not definitions:
|
||||
if show_warning_message:
|
||||
ConfigurationErrorMessage.getInstance().addFaultyContainers(definition_id)
|
||||
Logger.log("w", "Definition {definition} was not found!", definition = definition_id)
|
||||
return None
|
||||
|
|
|
@ -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.
|
||||
|
||||
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.
|
||||
def fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None:
|
||||
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:
|
||||
extruder_stack_0 = global_stack.extruderList[0]
|
||||
except IndexError:
|
||||
|
|
|
@ -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.
|
||||
|
||||
from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
|
||||
|
@ -8,6 +8,7 @@ from UM.Logger import Logger
|
|||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
|
||||
import cura.CuraApplication
|
||||
from UM.Signal import Signal
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
from cura.Settings.cura_empty_instance_containers import empty_intent_container
|
||||
|
||||
|
@ -29,6 +30,7 @@ class IntentManager(QObject):
|
|||
return cls.__instance
|
||||
|
||||
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]]:
|
||||
"""Gets the metadata dictionaries of all intent profiles for a given
|
||||
|
@ -189,3 +191,4 @@ class IntentManager(QObject):
|
|||
application.getMachineManager().setQualityGroupByQualityType(quality_type)
|
||||
if old_intent_category != intent_category:
|
||||
self.intentCategoryChanged.emit()
|
||||
self.intentCategoryChangedSignal.emit()
|
||||
|
|
|
@ -1611,7 +1611,7 @@ class MachineManager(QObject):
|
|||
if intent_category != "default":
|
||||
intent_display_name = IntentCategoryModel.translation(intent_category,
|
||||
"name",
|
||||
catalog.i18nc("@label", "Unknown"))
|
||||
intent_category.title())
|
||||
display_name = "{intent_name} - {the_rest}".format(intent_name = intent_display_name,
|
||||
the_rest = display_name)
|
||||
|
||||
|
@ -1778,3 +1778,31 @@ class MachineManager(QObject):
|
|||
abbr_machine += stripped_word
|
||||
|
||||
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 ""
|
||||
|
|
|
@ -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.
|
||||
|
||||
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl
|
||||
|
@ -18,6 +18,7 @@ if TYPE_CHECKING:
|
|||
from UM.Signal import Signal
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class CloudMaterialSync(QObject):
|
||||
"""
|
||||
Handles the synchronisation of material profiles with cloud accounts.
|
||||
|
@ -44,7 +45,6 @@ class CloudMaterialSync(QObject):
|
|||
break
|
||||
|
||||
def openSyncAllWindow(self):
|
||||
|
||||
self.reset()
|
||||
|
||||
if self.sync_all_dialog is None:
|
||||
|
|
|
@ -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.
|
||||
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["QT_OPENGL_DLL"] = "" # Security workaround: Don't need it, and introduces an attack vector, so set to nul.
|
||||
|
||||
from PyQt6.QtNetwork import QSslConfiguration, QSslSocket
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
|
|||
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
|
||||
from UM.Job import Job
|
||||
from UM.Preferences import Preferences
|
||||
from cura.CuraPackageManager import CuraPackageManager
|
||||
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
||||
|
@ -579,6 +580,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
is_printer_group = True
|
||||
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.
|
||||
self._dialog.setMachineConflict(machine_conflict)
|
||||
self._dialog.setIsPrinterGroup(is_printer_group)
|
||||
|
@ -599,6 +604,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
self._dialog.setExtruders(extruders)
|
||||
self._dialog.setVariantType(variant_type_name)
|
||||
self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity)
|
||||
self._dialog.setMissingPackagesMetadata(missing_package_metadata)
|
||||
self._dialog.show()
|
||||
|
||||
# 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"})
|
||||
for entry in metadata:
|
||||
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
|
||||
|
|
|
@ -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.
|
||||
|
||||
from PyQt6.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication, QUrl
|
||||
from PyQt6.QtGui import QDesktopServices
|
||||
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 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
|
||||
|
||||
import os
|
||||
|
@ -23,7 +28,7 @@ i18n_catalog = i18nCatalog("cura")
|
|||
class WorkspaceDialog(QObject):
|
||||
showDialogSignal = pyqtSignal()
|
||||
|
||||
def __init__(self, parent = None):
|
||||
def __init__(self, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._component = None
|
||||
self._context = None
|
||||
|
@ -59,6 +64,9 @@ class WorkspaceDialog(QObject):
|
|||
self._objects_on_plate = False
|
||||
self._is_printer_group = False
|
||||
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()
|
||||
qualityChangesConflictChanged = pyqtSignal()
|
||||
|
@ -79,6 +87,7 @@ class WorkspaceDialog(QObject):
|
|||
variantTypeChanged = pyqtSignal()
|
||||
extrudersChanged = pyqtSignal()
|
||||
isPrinterGroupChanged = pyqtSignal()
|
||||
missingPackagesChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(bool, notify = isPrinterGroupChanged)
|
||||
def isPrinterGroup(self) -> bool:
|
||||
|
@ -274,6 +283,21 @@ class WorkspaceDialog(QObject):
|
|||
self._has_quality_changes_conflict = quality_changes_conflict
|
||||
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]]:
|
||||
if "machine" in self._result and self.updatableMachinesModel.count <= 1:
|
||||
self._result["machine"] = None
|
||||
|
@ -360,6 +384,41 @@ class WorkspaceDialog(QObject):
|
|||
time.sleep(1 / 50)
|
||||
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:
|
||||
if self._view is None:
|
||||
self._createViewFromQML()
|
||||
|
|
|
@ -17,7 +17,7 @@ UM.Dialog
|
|||
minimumWidth: UM.Theme.getSize("popup_dialog").width
|
||||
minimumHeight: UM.Theme.getSize("popup_dialog").height
|
||||
width: minimumWidth
|
||||
|
||||
margin: UM.Theme.getSize("default_margin").width
|
||||
property int comboboxHeight: UM.Theme.getSize("default_margin").height
|
||||
|
||||
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
|
||||
{
|
||||
id: dialogSummaryItem
|
||||
width: parent.width
|
||||
width: verticalScrollBar.visible ? parent.width - verticalScrollBar.width - UM.Theme.getSize("default_margin").width : parent.width
|
||||
height: childrenRect.height
|
||||
anchors.margins: 10 * screenScaleFactor
|
||||
|
||||
|
@ -437,24 +445,89 @@ UM.Dialog
|
|||
{
|
||||
id: warningLabel
|
||||
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
|
||||
|
||||
rightButtons: [
|
||||
Cura.TertiaryButton
|
||||
{
|
||||
visible: !warning
|
||||
text: catalog.i18nc("@action:button", "Cancel")
|
||||
onClicked: reject()
|
||||
},
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
visible: !warning
|
||||
text: catalog.i18nc("@action:button", "Open")
|
||||
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()
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
# Copyright (c) 2015-2022 Ultimaker B.V.
|
||||
# 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.Math.Vector import Vector
|
||||
from UM.Logger import Logger
|
||||
from UM.Math.Matrix import Matrix
|
||||
from UM.Application import Application
|
||||
from UM.Message import Message
|
||||
from UM.Resources import Resources
|
||||
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.CuraPackageManager import CuraPackageManager
|
||||
from cura.Utils.Threading import call_on_qt_thread
|
||||
from cura.Snapshot import Snapshot
|
||||
|
||||
|
@ -34,6 +41,9 @@ import UM.Application
|
|||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
THUMBNAIL_PATH = "Metadata/thumbnail.png"
|
||||
MODEL_PATH = "3D/3dmodel.model"
|
||||
PACKAGE_METADATA_PATH = "Cura/packages.json"
|
||||
|
||||
class ThreeMFWriter(MeshWriter):
|
||||
def __init__(self):
|
||||
|
@ -46,7 +56,7 @@ class ThreeMFWriter(MeshWriter):
|
|||
}
|
||||
|
||||
self._unit_matrix_string = self._convertMatrixToString(Matrix())
|
||||
self._archive = None # type: Optional[zipfile.ZipFile]
|
||||
self._archive: Optional[zipfile.ZipFile] = None
|
||||
self._store_archive = False
|
||||
|
||||
def _convertMatrixToString(self, matrix):
|
||||
|
@ -132,11 +142,11 @@ class ThreeMFWriter(MeshWriter):
|
|||
def getArchive(self):
|
||||
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
|
||||
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
|
||||
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.
|
||||
model_file.compress_type = zipfile.ZIP_DEFLATED
|
||||
|
||||
|
@ -151,7 +161,7 @@ class ThreeMFWriter(MeshWriter):
|
|||
relations_file = zipfile.ZipInfo("_rels/.rels")
|
||||
relations_file.compress_type = zipfile.ZIP_DEFLATED
|
||||
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
|
||||
snapshot = self._createSnapshot()
|
||||
|
@ -160,28 +170,32 @@ class ThreeMFWriter(MeshWriter):
|
|||
thumbnail_buffer.open(QBuffer.OpenModeFlag.ReadWrite)
|
||||
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
|
||||
archive.writestr(thumbnail_file, thumbnail_buffer.data())
|
||||
|
||||
# Add PNG to content types file
|
||||
thumbnail_type = ET.SubElement(content_types, "Default", Extension = "png", ContentType = "image/png")
|
||||
# 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()
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
# the 3mf. So if it was already set, leave it to whatever it was.
|
||||
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("ModificationDate", current_time_string)
|
||||
|
@ -233,6 +247,55 @@ class ThreeMFWriter(MeshWriter):
|
|||
|
||||
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
|
||||
def _createSnapshot(self):
|
||||
Logger.log("d", "Creating thumbnail image...")
|
||||
|
|
|
@ -5,21 +5,23 @@ import sys
|
|||
from UM.Logger import Logger
|
||||
try:
|
||||
from . import ThreeMFWriter
|
||||
threemf_writer_was_imported = True
|
||||
except ImportError:
|
||||
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.Platform import Platform
|
||||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
def getMetaData():
|
||||
workspace_extension = "3mf"
|
||||
|
||||
metaData = {}
|
||||
|
||||
if "3MFWriter.ThreeMFWriter" in sys.modules:
|
||||
if threemf_writer_was_imported:
|
||||
metaData["mesh_writer"] = {
|
||||
"output": [{
|
||||
"extension": "3mf",
|
||||
|
@ -39,6 +41,7 @@ def getMetaData():
|
|||
|
||||
return metaData
|
||||
|
||||
|
||||
def register(app):
|
||||
if "3MFWriter.ThreeMFWriter" in sys.modules:
|
||||
return {"mesh_writer": ThreeMFWriter.ThreeMFWriter(),
|
||||
|
|
|
@ -5,7 +5,7 @@ import QtQuick 2.7
|
|||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import UM 1.3 as UM
|
||||
import UM 1.5 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
import "../components"
|
||||
|
@ -22,28 +22,23 @@ Item
|
|||
width: parent.width
|
||||
anchors.fill: parent
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: backupTitle
|
||||
text: catalog.i18nc("@title", "My Backups")
|
||||
font: UM.Theme.getFont("large")
|
||||
color: UM.Theme.getColor("text")
|
||||
Layout.fillWidth: true
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@empty_state",
|
||||
"You don't have any backups currently. Use the 'Backup Now' button to create one.")
|
||||
width: parent.width
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
wrapMode: Label.WordWrap
|
||||
visible: backupList.model.length == 0
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
BackupList
|
||||
|
@ -54,16 +49,13 @@ Item
|
|||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
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.")
|
||||
width: parent.width
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
wrapMode: Label.WordWrap
|
||||
visible: backupList.model.length > 4
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
BackupListFooter
|
||||
|
|
|
@ -23,7 +23,7 @@ Column
|
|||
{
|
||||
id: profileImage
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: "../images/backup.svg"
|
||||
source: Qt.resolvedUrl("../images/backup.svg")
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: Math.round(parent.width / 4)
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
executable_name = "CuraEngine"
|
||||
if Platform.isWindows():
|
||||
executable_name += ".exe"
|
||||
default_engine_location = executable_name
|
||||
self._default_engine_location = executable_name
|
||||
|
||||
search_path = [
|
||||
os.path.abspath(os.path.dirname(sys.executable)),
|
||||
|
@ -74,29 +74,29 @@ class CuraEngineBackend(QObject, Backend):
|
|||
for path in search_path:
|
||||
engine_path = os.path.join(path, executable_name)
|
||||
if os.path.isfile(engine_path):
|
||||
default_engine_location = engine_path
|
||||
self._default_engine_location = engine_path
|
||||
break
|
||||
|
||||
if Platform.isLinux() and not default_engine_location:
|
||||
if Platform.isLinux() and not self._default_engine_location:
|
||||
if not os.getenv("PATH"):
|
||||
raise OSError("There is something wrong with your Linux installation.")
|
||||
for pathdir in cast(str, os.getenv("PATH")).split(os.pathsep):
|
||||
execpath = os.path.join(pathdir, executable_name)
|
||||
if os.path.exists(execpath):
|
||||
default_engine_location = execpath
|
||||
self._default_engine_location = execpath
|
||||
break
|
||||
|
||||
application = CuraApplication.getInstance() #type: CuraApplication
|
||||
self._multi_build_plate_model = None #type: Optional[MultiBuildPlateModel]
|
||||
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")
|
||||
|
||||
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)
|
||||
application.getPreferences().addPreference("backend/location", default_engine_location)
|
||||
self._default_engine_location = os.path.abspath(self._default_engine_location)
|
||||
application.getPreferences().addPreference("backend/location", self._default_engine_location)
|
||||
|
||||
# Workaround to disable layer view processing if layer view is not active.
|
||||
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.
|
||||
: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.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.")
|
||||
|
|
|
@ -369,6 +369,9 @@ class StartSliceJob(Job):
|
|||
result["material_name"] = stack.material.getMetaDataEntry("name", "")
|
||||
result["material_brand"] = stack.material.getMetaDataEntry("brand", "")
|
||||
|
||||
result["quality_name"] = stack.quality.getMetaDataEntry("name", "")
|
||||
result["quality_changes_name"] = stack.qualityChanges.getMetaDataEntry("name")
|
||||
|
||||
# Renamed settings.
|
||||
result["print_bed_temperature"] = result["material_bed_temperature"]
|
||||
result["print_temperature"] = result["material_print_temperature"]
|
||||
|
|
|
@ -5,7 +5,7 @@ import QtQuick 2.15
|
|||
import QtQuick.Window 2.2
|
||||
import QtQuick.Controls 2.3
|
||||
|
||||
import UM 1.2 as UM
|
||||
import UM 1.5 as UM
|
||||
import Cura 1.6 as Cura
|
||||
|
||||
import DigitalFactory 1.0 as DF
|
||||
|
@ -64,12 +64,10 @@ Popup
|
|||
}
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: projectNameLabel
|
||||
text: "Project Name"
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
anchors
|
||||
{
|
||||
top: createNewLibraryProjectLabel.bottom
|
||||
|
@ -107,13 +105,12 @@ Popup
|
|||
}
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: errorWhileCreatingProjectLabel
|
||||
text: manager.projectCreationErrorText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("error")
|
||||
visible: manager.creatingNewProjectStatus == DF.RetrievalStatus.Failed
|
||||
anchors
|
||||
|
|
|
@ -37,7 +37,7 @@ Cura.RoundedRectangle
|
|||
width: UM.Theme.getSize("section").height
|
||||
height: width
|
||||
color: UM.Theme.getColor("text_link")
|
||||
source: "../images/arrow_down.svg"
|
||||
source: Qt.resolvedUrl("../images/arrow_down.svg")
|
||||
}
|
||||
|
||||
Label
|
||||
|
@ -65,7 +65,7 @@ Cura.RoundedRectangle
|
|||
{
|
||||
target: projectImage
|
||||
color: UM.Theme.getColor("text_link")
|
||||
source: "../images/arrow_down.svg"
|
||||
source: Qt.resolvedUrl("../images/arrow_down.svg")
|
||||
}
|
||||
PropertyChanges
|
||||
{
|
||||
|
@ -88,7 +88,7 @@ Cura.RoundedRectangle
|
|||
{
|
||||
target: projectImage
|
||||
color: UM.Theme.getColor("text_link")
|
||||
source: "../images/arrow_down.svg"
|
||||
source: Qt.resolvedUrl("../images/arrow_down.svg")
|
||||
}
|
||||
PropertyChanges
|
||||
{
|
||||
|
@ -111,7 +111,7 @@ Cura.RoundedRectangle
|
|||
{
|
||||
target: projectImage
|
||||
color: UM.Theme.getColor("action_button_disabled_text")
|
||||
source: "../images/update.svg"
|
||||
source: Qt.resolvedUrl("../images/update.svg")
|
||||
}
|
||||
PropertyChanges
|
||||
{
|
||||
|
|
|
@ -83,13 +83,12 @@ Item
|
|||
}
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: emptyProjectLabel
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Select a project to view its files."
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("setting_category_text")
|
||||
|
||||
Connections
|
||||
|
@ -102,14 +101,13 @@ Item
|
|||
}
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: noFilesInProjectLabel
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: (manager.digitalFactoryFileModel.count == 0 && !emptyProjectLabel.visible && !retrievingFilesBusyIndicator.visible)
|
||||
text: "No supported files in this project."
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("setting_category_text")
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import QtQuick 2.10
|
||||
import QtQuick.Controls 2.3
|
||||
|
||||
import UM 1.2 as UM
|
||||
import UM 1.5 as UM
|
||||
import Cura 1.6 as Cura
|
||||
|
||||
Cura.RoundedRectangle
|
||||
|
@ -58,34 +58,31 @@ Cura.RoundedRectangle
|
|||
width: parent.width - x - UM.Theme.getSize("default_margin").width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: displayNameLabel
|
||||
width: parent.width
|
||||
height: Math.round(parent.height / 3)
|
||||
elide: Text.ElideRight
|
||||
color: UM.Theme.getColor("text")
|
||||
font: UM.Theme.getFont("default_bold")
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: usernameLabel
|
||||
width: parent.width
|
||||
height: Math.round(parent.height / 3)
|
||||
elide: Text.ElideRight
|
||||
color: UM.Theme.getColor("small_button_text")
|
||||
font: UM.Theme.getFont("default")
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: lastUpdatedLabel
|
||||
width: parent.width
|
||||
height: Math.round(parent.height / 3)
|
||||
elide: Text.ElideRight
|
||||
color: UM.Theme.getColor("small_button_text")
|
||||
font: UM.Theme.getFont("default")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,9 @@ import DigitalFactory 1.0 as DF
|
|||
Item
|
||||
{
|
||||
id: base
|
||||
|
||||
property variant catalog: UM.I18nCatalog { name: "cura" }
|
||||
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
|
||||
|
@ -44,14 +47,13 @@ Item
|
|||
cardMouseAreaEnabled: false
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: fileNameLabel
|
||||
anchors.top: projectSummaryCard.bottom
|
||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||
text: "Cura project name"
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
|
||||
|
||||
|
@ -110,13 +112,12 @@ Item
|
|||
}
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: emptyProjectLabel
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Select a project to view its files."
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("setting_category_text")
|
||||
|
||||
Connections
|
||||
|
@ -129,14 +130,13 @@ Item
|
|||
}
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: noFilesInProjectLabel
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: (manager.digitalFactoryFileModel.count == 0 && !emptyProjectLabel.visible && !retrievingFilesBusyIndicator.visible)
|
||||
text: "No supported files in this project."
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("setting_category_text")
|
||||
}
|
||||
|
||||
|
@ -193,53 +193,29 @@ Item
|
|||
text: "Save"
|
||||
enabled: (asProjectCheckbox.checked || asSlicedCheckbox.checked) && dfFilenameTextfield.text.length >= 1 && dfFilenameTextfield.state !== 'invalid'
|
||||
|
||||
onClicked:
|
||||
{
|
||||
let saveAsFormats = [];
|
||||
if (asProjectCheckbox.checked)
|
||||
{
|
||||
saveAsFormats.push("3mf");
|
||||
}
|
||||
if (asSlicedCheckbox.checked)
|
||||
{
|
||||
saveAsFormats.push("ufp");
|
||||
}
|
||||
manager.saveFileToSelectedProject(dfFilenameTextfield.text, saveAsFormats);
|
||||
}
|
||||
onClicked: manager.saveFileToSelectedProject(dfFilenameTextfield.text, asProjectComboBox.currentValue)
|
||||
busy: false
|
||||
}
|
||||
|
||||
Row
|
||||
Cura.ComboBox
|
||||
{
|
||||
id: asProjectComboBox
|
||||
|
||||
id: saveAsFormatRow
|
||||
width: UM.Theme.getSize("combobox_wide").width
|
||||
height: saveButton.height
|
||||
anchors.verticalCenter: saveButton.verticalCenter
|
||||
anchors.right: saveButton.left
|
||||
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
|
||||
checked: UM.Backend.state == UM.Backend.Done
|
||||
text: "Save print file"
|
||||
font: UM.Theme.getFont("medium")
|
||||
}
|
||||
currentIndex: UM.Backend.state == UM.Backend.Done ? 0 : 1
|
||||
textRole: "text"
|
||||
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:
|
||||
|
|
|
@ -99,18 +99,17 @@ Item
|
|||
{
|
||||
id: digitalFactoryImage
|
||||
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
|
||||
width: parent.width - 2 * UM.Theme.getSize("thick_margin").width
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: noLibraryProjectsLabel
|
||||
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."
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
|
||||
Cura.TertiaryButton
|
||||
|
|
|
@ -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.
|
||||
|
||||
import json
|
||||
import threading
|
||||
from json import JSONDecodeError
|
||||
|
@ -40,14 +41,15 @@ class DFFileExportAndUploadManager:
|
|||
on_upload_finished: Callable[[], Any],
|
||||
on_upload_progress: Callable[[int], Any]) -> None:
|
||||
|
||||
self._file_handlers = file_handlers # type: Dict[str, FileHandler]
|
||||
self._nodes = nodes # type: List[SceneNode]
|
||||
self._library_project_id = library_project_id # type: str
|
||||
self._library_project_name = library_project_name # type: str
|
||||
self._file_name = file_name # type: str
|
||||
self._upload_jobs = [] # type: List[ExportFileJob]
|
||||
self._formats = formats # type: List[str]
|
||||
self._file_handlers: Dict[str, FileHandler] = file_handlers
|
||||
self._nodes: List[SceneNode] = nodes
|
||||
self._library_project_id: str = library_project_id
|
||||
self._library_project_name: str = library_project_name
|
||||
self._file_name: str = file_name
|
||||
self._upload_jobs: List[ExportFileJob] = []
|
||||
self._formats: List[str] = formats
|
||||
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
|
||||
self._on_upload_error = on_upload_error
|
||||
|
@ -59,7 +61,7 @@ class DFFileExportAndUploadManager:
|
|||
# show the success message (once both upload jobs are done)
|
||||
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(
|
||||
title = "Uploading...",
|
||||
|
@ -113,7 +115,8 @@ class DFFileExportAndUploadManager:
|
|||
content_type = job.getMimeType(),
|
||||
job_name = job.getFileName(),
|
||||
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)
|
||||
|
||||
|
@ -125,11 +128,17 @@ class DFFileExportAndUploadManager:
|
|||
"""
|
||||
if isinstance(file_upload_response, DFLibraryFileUploadResponse):
|
||||
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):
|
||||
file_name = file_upload_response.job_name if file_upload_response.job_name is not None else ""
|
||||
else:
|
||||
Logger.log("e", "Wrong response type received. Aborting uploading file to the Digital Library")
|
||||
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:
|
||||
self.progress_message.show()
|
||||
self._file_upload_job_metadata[file_name]["file_upload_response"] = file_upload_response
|
||||
|
@ -145,6 +154,8 @@ class DFFileExportAndUploadManager:
|
|||
on_progress = self._onUploadProgress,
|
||||
on_error = self._onUploadError)
|
||||
|
||||
self._handleNextUploadJob()
|
||||
|
||||
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
|
||||
|
@ -325,8 +336,14 @@ class DFFileExportAndUploadManager:
|
|||
message.hide()
|
||||
|
||||
def start(self) -> None:
|
||||
for job in self._upload_jobs:
|
||||
self._handleNextUploadJob()
|
||||
|
||||
def _handleNextUploadJob(self):
|
||||
try:
|
||||
job = self._upload_jobs.pop(0)
|
||||
job.start()
|
||||
except IndexError:
|
||||
pass # Empty list, do nothing.
|
||||
|
||||
def initializeFileUploadJobMetadata(self) -> Dict[str, Any]:
|
||||
metadata = {}
|
||||
|
|
|
@ -39,8 +39,8 @@ class DFFileUploader:
|
|||
:param on_error: The method to be called when an error occurs.
|
||||
"""
|
||||
|
||||
self._http = http # type: HttpRequestManager
|
||||
self._df_file = df_file # type: Union[DFLibraryFileUploadResponse, DFPrintJobUploadResponse]
|
||||
self._http: HttpRequestManager = http
|
||||
self._df_file: Union[DFLibraryFileUploadResponse, DFPrintJobUploadResponse] = df_file
|
||||
self._file_name = ""
|
||||
if isinstance(self._df_file, DFLibraryFileUploadResponse):
|
||||
self._file_name = self._df_file.file_name
|
||||
|
@ -51,7 +51,7 @@ class DFFileUploader:
|
|||
self._file_name = ""
|
||||
else:
|
||||
raise TypeError("Incorrect input type")
|
||||
self._data = data # type: bytes
|
||||
self._data: bytes = data
|
||||
|
||||
self._on_finished = on_finished
|
||||
self._on_success = on_success
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional
|
||||
|
||||
from .BaseModel import BaseModel
|
||||
|
||||
|
||||
# Model that represents the request to upload a print job to the cloud
|
||||
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.
|
||||
|
||||
:param job_name: The name of the print job.
|
||||
|
@ -18,4 +20,5 @@ class DFPrintJobUploadRequest(BaseModel):
|
|||
self.file_size = file_size
|
||||
self.content_type = content_type
|
||||
self.library_project_id = library_project_id
|
||||
self.source_file_id = source_file_id
|
||||
super().__init__(**kwargs)
|
||||
|
|
|
@ -40,7 +40,7 @@ class DigitalFactoryApiClient:
|
|||
DEFAULT_REQUEST_TIMEOUT = 10 # seconds
|
||||
|
||||
# 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:
|
||||
"""Initializes a new digital factory API client.
|
||||
|
@ -54,7 +54,7 @@ class DigitalFactoryApiClient:
|
|||
self._scope = JsonDecoratorScope(UltimakerCloudScope(application))
|
||||
self._http = HttpRequestManager.getInstance()
|
||||
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._projects_pagination_mgr = PaginationManager(limit = projects_limit_per_page) if projects_limit_per_page else None # type: Optional[PaginationManager]
|
||||
|
|
|
@ -5,7 +5,7 @@ import QtQuick 2.2
|
|||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.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 Cura 1.0 as Cura
|
||||
|
@ -147,8 +147,6 @@ Cura.MachineAction
|
|||
return catalog.i18nc("@label","Firmware update failed due to missing firmware.");
|
||||
}
|
||||
}
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
UM.ProgressBar
|
||||
|
|
|
@ -31,7 +31,7 @@ UM.Dialog
|
|||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
|
||||
text: catalog.i18nc("@action:label", "Height (mm)")
|
||||
Layout.alignment: Qt.AlignmentFlag.AlignVCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
MouseArea {
|
||||
id: peak_height_label
|
||||
|
@ -64,7 +64,7 @@ UM.Dialog
|
|||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
|
||||
text: catalog.i18nc("@action:label", "Base (mm)")
|
||||
Layout.alignment: Qt.AlignmentFlag.AlignVCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
MouseArea
|
||||
{
|
||||
|
@ -98,7 +98,7 @@ UM.Dialog
|
|||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
|
||||
text: catalog.i18nc("@action:label", "Width (mm)")
|
||||
Layout.alignment: Qt.AlignmentFlag.AlignVCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
MouseArea {
|
||||
id: width_label
|
||||
|
@ -132,7 +132,7 @@ UM.Dialog
|
|||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
|
||||
text: catalog.i18nc("@action:label", "Depth (mm)")
|
||||
Layout.alignment: Qt.AlignmentFlag.AlignVCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
MouseArea {
|
||||
id: depth_label
|
||||
|
@ -166,7 +166,7 @@ UM.Dialog
|
|||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
|
||||
text: ""
|
||||
Layout.alignment: Qt.AlignmentFlag.AlignVCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
MouseArea {
|
||||
id: lighter_is_higher_label
|
||||
|
@ -203,7 +203,7 @@ UM.Dialog
|
|||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
|
||||
text: catalog.i18nc("@action:label", "Color Model")
|
||||
Layout.alignment: Qt.AlignmentFlag.AlignVCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
MouseArea {
|
||||
id: color_model_label
|
||||
|
@ -240,7 +240,7 @@ UM.Dialog
|
|||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
|
||||
text: catalog.i18nc("@action:label", "1mm Transmittance (%)")
|
||||
Layout.alignment: Qt.AlignmentFlag.AlignVCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
MouseArea {
|
||||
id: transmittance_label
|
||||
|
@ -272,7 +272,7 @@ UM.Dialog
|
|||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
|
||||
text: catalog.i18nc("@action:label", "Smoothing")
|
||||
Layout.alignment: Qt.AlignmentFlag.AlignVCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
MouseArea
|
||||
{
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import QtQuick 2.10
|
||||
import QtQuick.Controls 2.3
|
||||
|
||||
import UM 1.3 as UM
|
||||
import UM 1.5 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
|
||||
|
@ -58,11 +58,10 @@ Item
|
|||
|
||||
spacing: base.columnSpacing
|
||||
|
||||
Label // Title Label
|
||||
UM.Label // Title Label
|
||||
{
|
||||
text: catalog.i18nc("@title:label", "Nozzle Settings")
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Cura.NumericTextFieldWithUnit // "Nozzle size"
|
||||
|
|
|
@ -5,7 +5,7 @@ import QtQuick 2.10
|
|||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import UM 1.3 as UM
|
||||
import UM 1.5 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
|
||||
|
@ -47,16 +47,14 @@ Item
|
|||
Column
|
||||
{
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignmentFlag.AlignTop
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
spacing: base.columnSpacing
|
||||
|
||||
Label // Title Label
|
||||
UM.Label // Title Label
|
||||
{
|
||||
text: catalog.i18nc("@title:label", "Printer Settings")
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
renderType: Text.NativeRendering
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
@ -178,16 +176,14 @@ Item
|
|||
Column
|
||||
{
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignmentFlag.AlignTop
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
spacing: base.columnSpacing
|
||||
|
||||
Label // Title Label
|
||||
UM.Label // Title Label
|
||||
{
|
||||
text: catalog.i18nc("@title:label", "Printhead Settings")
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
renderType: Text.NativeRendering
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
|
67
plugins/Marketplace/InstallMissingPackagesDialog.py
Normal file
67
plugins/Marketplace/InstallMissingPackagesDialog.py
Normal 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()
|
|
@ -108,7 +108,7 @@ class LocalPackageList(PackageList):
|
|||
:param reply: A reply containing information about a number of packages.
|
||||
"""
|
||||
response_data = HttpRequestManager.readJSON(reply)
|
||||
if "data" not in response_data:
|
||||
if response_data is None or "data" not in response_data:
|
||||
Logger.error(
|
||||
f"Could not interpret the server's response. Missing 'data' from response data. Keys in response: {response_data.keys()}")
|
||||
return
|
||||
|
|
|
@ -3,15 +3,15 @@
|
|||
|
||||
import os.path
|
||||
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 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 .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 .RemotePackageList import RemotePackageList # To register this type with QML.
|
||||
|
||||
|
||||
class Marketplace(Extension, QObject):
|
||||
|
@ -22,7 +22,6 @@ class Marketplace(Extension, QObject):
|
|||
QObject.__init__(self, parent)
|
||||
Extension.__init__(self)
|
||||
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._material_package_list: Optional[RemotePackageList] = None
|
||||
|
@ -41,6 +40,7 @@ class Marketplace(Extension, QObject):
|
|||
|
||||
self._tab_shown: int = 0
|
||||
self._restart_needed = False
|
||||
self.missingPackageDialog = None
|
||||
|
||||
def getTabShown(self) -> int:
|
||||
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 self._window is None:
|
||||
self._plugin_registry = PluginRegistry.getInstance()
|
||||
self._plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.checkIfRestartNeeded)
|
||||
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
||||
plugin_registry = PluginRegistry.getInstance()
|
||||
plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.checkIfRestartNeeded)
|
||||
plugin_path = plugin_registry.getPluginPath(self.getPluginId())
|
||||
if plugin_path is None:
|
||||
plugin_path = os.path.dirname(__file__)
|
||||
path = os.path.join(plugin_path, "resources", "qml", "Marketplace.qml")
|
||||
|
@ -103,8 +103,11 @@ class Marketplace(Extension, QObject):
|
|||
self.setTabShown(1)
|
||||
|
||||
def checkIfRestartNeeded(self) -> None:
|
||||
if self._window is None:
|
||||
return
|
||||
|
||||
if self._package_manager.hasPackagesToRemoveOrInstall or \
|
||||
cast(PluginRegistry, self._plugin_registry).getCurrentSessionActivationChangedPlugins():
|
||||
PluginRegistry.getInstance().getCurrentSessionActivationChangedPlugins():
|
||||
self._restart_needed = True
|
||||
else:
|
||||
self._restart_needed = False
|
||||
|
@ -115,3 +118,15 @@ class Marketplace(Extension, QObject):
|
|||
@pyqtProperty(bool, notify = showRestartNotificationChanged)
|
||||
def showRestartNotification(self) -> bool:
|
||||
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()
|
||||
|
|
46
plugins/Marketplace/MissingPackageList.py
Normal file
46
plugins/Marketplace/MissingPackageList.py
Normal 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()
|
||||
|
||||
|
|
@ -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:
|
||||
if reply:
|
||||
try:
|
||||
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}")
|
||||
self._package_manager.packageInstallingFailed.emit(package_id)
|
||||
|
||||
|
|
|
@ -84,6 +84,20 @@ class PackageModel(QObject):
|
|||
|
||||
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()
|
||||
def _processUpdatedPackages(self):
|
||||
self.setCanUpdate(self._package_manager.checkIfPackageCanUpdate(self._package_id))
|
||||
|
@ -385,3 +399,14 @@ class PackageModel(QObject):
|
|||
def canUpdate(self) -> bool:
|
||||
"""Flag indicating if the package can be updated"""
|
||||
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
|
||||
|
|
|
@ -28,6 +28,7 @@ class RemotePackageList(PackageList):
|
|||
self._package_type_filter = ""
|
||||
self._requested_search_string = ""
|
||||
self._current_search_string = ""
|
||||
self._search_type = "search"
|
||||
self._request_url = self._initialRequestUrl()
|
||||
self._ongoing_requests["get_packages"] = None
|
||||
self.isLoadingChanged.connect(self._onLoadingChanged)
|
||||
|
@ -100,7 +101,7 @@ class RemotePackageList(PackageList):
|
|||
if self._package_type_filter != "":
|
||||
request_url += f"&package_type={self._package_type_filter}"
|
||||
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
|
||||
|
||||
def _parseResponse(self, reply: "QNetworkReply") -> None:
|
||||
|
|
|
@ -14,4 +14,4 @@ def register(app):
|
|||
"""
|
||||
Register the plug-in object with Uranium.
|
||||
"""
|
||||
return { "extension": [Marketplace(), SyncOrchestrator(app)] }
|
||||
return { "extension": [SyncOrchestrator(app), Marketplace()] }
|
||||
|
|
|
@ -67,7 +67,7 @@ UM.Dialog
|
|||
Image
|
||||
{
|
||||
id: packageIcon
|
||||
source: model.icon_url || Qt.resolvedUrl("../../images/placeholder.svg")
|
||||
source: model.icon_url || Qt.resolvedUrl("../images/placeholder.svg")
|
||||
height: lineHeight
|
||||
width: height
|
||||
sourceSize.height: height
|
||||
|
@ -109,7 +109,7 @@ UM.Dialog
|
|||
Image
|
||||
{
|
||||
id: packageIcon
|
||||
source: model.icon_url || Qt.resolvedUrl("../../images/placeholder.svg")
|
||||
source: model.icon_url || Qt.resolvedUrl("../images/placeholder.svg")
|
||||
height: lineHeight
|
||||
width: height
|
||||
sourceSize.height: height
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -42,16 +42,13 @@ UM.Dialog
|
|||
source: UM.Theme.getIcon("Certificate", "high")
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@text", "Please read and agree with the plugin licence.")
|
||||
color: UM.Theme.getColor("text")
|
||||
font: UM.Theme.getFont("large")
|
||||
anchors.verticalCenter: icon.verticalCenter
|
||||
height: UM.Theme.getSize("marketplace_large_icon").height
|
||||
verticalAlignment: Qt.AlignmentFlag.AlignVCenter
|
||||
wrapMode: Text.Wrap
|
||||
renderType: Text.NativeRendering
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import QtQuick.Controls 2.15
|
|||
import QtQuick.Layouts 1.15
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import UM 1.2 as UM
|
||||
import UM 1.5 as UM
|
||||
import Cura 1.6 as Cura
|
||||
|
||||
Window
|
||||
|
@ -16,6 +16,10 @@ Window
|
|||
|
||||
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
|
||||
minimumHeight: UM.Theme.getSize("modal_window_minimum").height
|
||||
width: minimumWidth
|
||||
|
@ -67,7 +71,7 @@ Window
|
|||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: childrenRect.height + UM.Theme.getSize("default_margin").height
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: pageTitle
|
||||
anchors
|
||||
|
@ -80,13 +84,13 @@ Window
|
|||
}
|
||||
|
||||
font: UM.Theme.getFont("large")
|
||||
color: UM.Theme.getColor("text")
|
||||
text: content.item ? content.item.pageTitle: catalog.i18nc("@title", "Loading...")
|
||||
}
|
||||
}
|
||||
|
||||
OnboardBanner
|
||||
{
|
||||
id: onBoardBanner
|
||||
visible: content.item && content.item.bannerVisible
|
||||
text: content.item && content.item.bannerText
|
||||
icon: content.item && content.item.bannerIcon
|
||||
|
@ -101,6 +105,7 @@ Window
|
|||
// Search & Top-Level Tabs
|
||||
Item
|
||||
{
|
||||
id: searchHeader
|
||||
implicitHeight: childrenRect.height
|
||||
implicitWidth: parent.width - 2 * UM.Theme.getSize("default_margin").width
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
@ -187,7 +192,7 @@ Window
|
|||
{
|
||||
text: catalog.i18nc("@info", "Search in the browser")
|
||||
iconSource: UM.Theme.getIcon("LinkExternal")
|
||||
visible: pageSelectionTabBar.currentItem.hasSearch
|
||||
visible: pageSelectionTabBar.currentItem.hasSearch && searchHeader.visible
|
||||
isIconOnRightSide: true
|
||||
height: fontMetrics.height
|
||||
textFont: fontMetrics.font
|
||||
|
|
15
plugins/Marketplace/resources/qml/MissingPackages.qml
Normal file
15
plugins/Marketplace/resources/qml/MissingPackages.qml
Normal 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
|
||||
}
|
|
@ -51,7 +51,7 @@ UM.Dialog
|
|||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: licenseModel.iconUrl || Qt.resolvedUrl("../../images/placeholder.svg")
|
||||
source: licenseModel.iconUrl || Qt.resolvedUrl("../images/placeholder.svg")
|
||||
mipmap: true
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,8 @@ Rectangle
|
|||
}
|
||||
width: UM.Theme.getSize("banner_icon_size").width
|
||||
height: UM.Theme.getSize("banner_icon_size").height
|
||||
|
||||
color: UM.Theme.getColor("primary_text")
|
||||
}
|
||||
|
||||
UM.SimpleButton
|
||||
|
|
|
@ -18,6 +18,8 @@ Rectangle
|
|||
height: childrenRect.height
|
||||
color: UM.Theme.getColor("main_background")
|
||||
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
|
||||
{
|
||||
|
@ -29,16 +31,13 @@ Rectangle
|
|||
|
||||
anchors.fill: parent
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: descriptionLabel
|
||||
width: parent.width
|
||||
|
||||
text: packageData.description
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
maximumLineCount: 2
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
visible: text !== ""
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ Item
|
|||
property bool showInstallButton: 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
|
||||
height: UM.Theme.getSize("card").height
|
||||
|
@ -87,11 +89,18 @@ Item
|
|||
Layout.preferredWidth: parent.width
|
||||
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
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
verticalAlignment: Text.AlignTop
|
||||
}
|
||||
VerifiedIcon
|
||||
|
@ -100,18 +109,17 @@ Item
|
|||
visible: packageData.isCheckedByUltimaker
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: packageVersionLabel
|
||||
text: packageData.packageVersion
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Button
|
||||
{
|
||||
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?
|
||||
leftPadding: UM.Theme.getSize("narrow_margin").width
|
||||
|
@ -119,8 +127,8 @@ Item
|
|||
topPadding: UM.Theme.getSize("narrow_margin").width
|
||||
bottomPadding: UM.Theme.getSize("narrow_margin").width
|
||||
|
||||
Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width + 2 * padding
|
||||
Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").width + 2 * padding
|
||||
width: UM.Theme.getSize("card_tiny_icon").width + 2 * padding
|
||||
height: UM.Theme.getSize("card_tiny_icon").width + 2 * padding
|
||||
contentItem: UM.ColorImage
|
||||
{
|
||||
source: UM.Theme.getIcon("LinkExternal")
|
||||
|
@ -155,12 +163,13 @@ Item
|
|||
spacing: UM.Theme.getSize("narrow_margin").width
|
||||
|
||||
// label "By"
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: authorBy
|
||||
visible: !packageData.isMissingPackageInformation
|
||||
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")
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
|
@ -168,6 +177,7 @@ Item
|
|||
// clickable author name
|
||||
Item
|
||||
{
|
||||
visible: !packageData.isMissingPackageInformation
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: authorBy.height
|
||||
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
|
||||
{
|
||||
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
|
||||
|
||||
button_style: !packageData.isActive
|
||||
|
@ -202,7 +231,7 @@ Item
|
|||
ManageButton
|
||||
{
|
||||
id: installManageButton
|
||||
visible: showInstallButton && (packageData.canDowngrade || !packageData.isBundled)
|
||||
visible: showInstallButton && (packageData.canDowngrade || !packageData.isBundled) && !packageData.isMissingPackageInformation
|
||||
enabled: !packageData.busy
|
||||
busy: packageData.busy
|
||||
button_style: !(packageData.isInstalled || packageData.isToBeInstalled)
|
||||
|
@ -232,7 +261,7 @@ Item
|
|||
ManageButton
|
||||
{
|
||||
id: updateManageButton
|
||||
visible: showUpdateButton && packageData.canUpdate
|
||||
visible: showUpdateButton && packageData.canUpdate && !packageData.isMissingPackageInformation
|
||||
enabled: !packageData.busy
|
||||
busy: packageData.busy
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
|
|
@ -31,7 +31,7 @@ Item
|
|||
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
Layout.alignment: Qt.AlignmentFlag.AlignVCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredHeight: UM.Theme.getSize("action_button").height
|
||||
Layout.preferredWidth: height
|
||||
|
||||
|
@ -45,14 +45,13 @@ Item
|
|||
iconSize: height - leftPadding * 2
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
Layout.alignment: Qt.AlignmentFlag.AlignVCenter
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: detailPage.title
|
||||
font: UM.Theme.getFont("large")
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -56,13 +56,9 @@ Rectangle
|
|||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
|
||||
anchors.verticalCenter: downloadsIcon.verticalCenter
|
||||
|
||||
color: UM.Theme.getColor("text")
|
||||
font: UM.Theme.getFont("default")
|
||||
text: packageData.downloadCount
|
||||
}
|
||||
}
|
||||
|
@ -78,25 +74,22 @@ Rectangle
|
|||
topPadding: 0
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: parent.width - parent.padding * 2
|
||||
|
||||
text: catalog.i18nc("@header", "Description")
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: parent.width - parent.padding * 2
|
||||
|
||||
text: packageData.formattedDescription
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
linkColor: UM.Theme.getColor("text_link")
|
||||
wrapMode: Text.Wrap
|
||||
textFormat: Text.RichText
|
||||
|
||||
onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"])
|
||||
|
@ -110,13 +103,12 @@ Rectangle
|
|||
visible: packageData.packageType === "material"
|
||||
spacing: 0
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: parent.width
|
||||
|
||||
text: catalog.i18nc("@header", "Compatible printers")
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
|
@ -124,25 +116,23 @@ Rectangle
|
|||
{
|
||||
model: packageData.compatiblePrinters
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: compatiblePrinterColumn.width
|
||||
|
||||
text: modelData
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: parent.width
|
||||
|
||||
visible: packageData.compatiblePrinters.length == 0
|
||||
text: "(" + catalog.i18nc("@info", "No compatibility information") + ")"
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
@ -155,13 +145,12 @@ Rectangle
|
|||
visible: packageData.packageType === "material"
|
||||
spacing: 0
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: parent.width
|
||||
|
||||
text: catalog.i18nc("@header", "Compatible support materials")
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
|
@ -169,25 +158,23 @@ Rectangle
|
|||
{
|
||||
model: packageData.compatibleSupportMaterials
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: compatibleSupportMaterialColumn.width
|
||||
|
||||
text: modelData
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: parent.width
|
||||
|
||||
visible: packageData.compatibleSupportMaterials.length == 0
|
||||
text: "(" + catalog.i18nc("@info No materials", "None") + ")"
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
@ -199,23 +186,21 @@ Rectangle
|
|||
visible: packageData.packageType === "material"
|
||||
spacing: 0
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: parent.width
|
||||
|
||||
text: catalog.i18nc("@header", "Compatible with Material Station")
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: parent.width
|
||||
|
||||
text: packageData.isCompatibleMaterialStation ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No")
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
@ -227,23 +212,21 @@ Rectangle
|
|||
visible: packageData.packageType === "material"
|
||||
spacing: 0
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: parent.width
|
||||
|
||||
text: catalog.i18nc("@header", "Optimized for Air Manager")
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: parent.width
|
||||
|
||||
text: packageData.isCompatibleAirManager ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No")
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import UM 1.0 as UM
|
||||
import UM 1.5 as UM
|
||||
|
||||
TabButton
|
||||
{
|
||||
|
@ -22,11 +22,10 @@ TabButton
|
|||
border.width: UM.Theme.getSize("thick_lining").width
|
||||
}
|
||||
|
||||
contentItem: Label
|
||||
contentItem: UM.Label
|
||||
{
|
||||
text: parent.text
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
width: contentWidth
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ ListView
|
|||
|
||||
color: UM.Theme.getColor("detail_background")
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: sectionHeaderText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
@ -48,7 +48,6 @@ ListView
|
|||
|
||||
text: section
|
||||
font: UM.Theme.getFont("large")
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,10 +61,13 @@ ListView
|
|||
|
||||
hoverEnabled: true
|
||||
onClicked:
|
||||
{
|
||||
if (!model.package.isMissingPackageInformation)
|
||||
{
|
||||
packages.selectedPackage = model.package;
|
||||
contextStack.push(packageDetailsComponent);
|
||||
}
|
||||
}
|
||||
|
||||
PackageCard
|
||||
{
|
||||
|
|
|
@ -14,7 +14,7 @@ UM.SimpleButton
|
|||
|
||||
width: UM.Theme.getSize("save_button_specs_icons").width
|
||||
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
|
||||
color: UM.Theme.getColor("text_scene")
|
||||
hoverColor: UM.Theme.getColor("text_scene_hover")
|
||||
|
|
|
@ -122,6 +122,7 @@ Rectangle
|
|||
}
|
||||
visible: !isNetworkConfigured && isNetworkConfigurable
|
||||
width: childrenRect.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.ColorImage
|
||||
{
|
||||
|
@ -132,7 +133,7 @@ Rectangle
|
|||
width: UM.Theme.getSize("icon_indicator").width
|
||||
height: UM.Theme.getSize("icon_indicator").height
|
||||
}
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: manageQueueText
|
||||
anchors
|
||||
|
@ -144,7 +145,6 @@ Rectangle
|
|||
color: UM.Theme.getColor("text_link")
|
||||
font: UM.Theme.getFont("medium")
|
||||
text: catalog.i18nc("@label link to technical assistance", "View user manuals online")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
MouseArea
|
||||
{
|
||||
|
@ -155,14 +155,13 @@ Rectangle
|
|||
onExited: manageQueueText.font.underline = false
|
||||
}
|
||||
}
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: noConnectionLabel
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: !isNetworkConfigurable
|
||||
text: catalog.i18nc("@info", "In order to monitor your print from Cura, please connect the printer.")
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
wrapMode: Text.WordWrap
|
||||
width: contentWidth
|
||||
}
|
||||
|
|
|
@ -232,7 +232,7 @@ UM.Dialog
|
|||
}
|
||||
|
||||
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
|
||||
id: settingsPanel
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: scriptSpecsHeader
|
||||
text: manager.selectedScriptIndex == -1 ? catalog.i18nc("@label", "Settings") : base.activeScriptName
|
||||
|
@ -262,7 +262,6 @@ UM.Dialog
|
|||
elide: Text.ElideRight
|
||||
height: 20 * screenScaleFactor
|
||||
font: UM.Theme.getFont("large_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
|
||||
ListView
|
||||
|
@ -475,7 +474,7 @@ UM.Dialog
|
|||
}
|
||||
toolTipContentAlignment: UM.Enums.ContentAlignment.AlignLeft
|
||||
onClicked: dialog.show()
|
||||
// iconSource: "Script.svg"
|
||||
iconSource: Qt.resolvedUrl("Script.svg")
|
||||
fixedWidthMode: false
|
||||
}
|
||||
|
||||
|
|
|
@ -330,7 +330,7 @@ class PauseAtHeight(Script):
|
|||
|
||||
current_height = current_z - layer_0_z
|
||||
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
|
||||
else:
|
||||
|
|
|
@ -106,26 +106,34 @@ Item
|
|||
{
|
||||
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.
|
||||
onChildrenRectChanged:
|
||||
// Automatically set the width to fit the widest MenuItem
|
||||
// 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
|
||||
popup.implicitWidth = childrenRect.width
|
||||
}
|
||||
onPositioningComplete:
|
||||
var result = 0;
|
||||
var padding = 0;
|
||||
for (var i = 0; i < fileProviderRepeater.count; ++i) {
|
||||
var item = fileProviderRepeater.itemAt(i);
|
||||
if (item.hasOwnProperty("implicitWidth"))
|
||||
{
|
||||
popup.implicitHeight = childrenRect.height
|
||||
popup.implicitWidth = childrenRect.width
|
||||
var itemWidth = item.implicitWidth;
|
||||
result = Math.max(itemWidth, result);
|
||||
padding = Math.max(item.padding, padding);
|
||||
}
|
||||
}
|
||||
return result + padding * 2;
|
||||
}
|
||||
width: setWidth()
|
||||
|
||||
Repeater
|
||||
{
|
||||
id: fileProviderRepeater
|
||||
model: prepareMenu.fileProviderModel
|
||||
delegate: Button
|
||||
{
|
||||
leftPadding: 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
|
||||
hoverEnabled: true
|
||||
|
||||
|
|
|
@ -80,6 +80,18 @@ class RemovableDriveOutputDevice(OutputDevice):
|
|||
if extension: # Not empty string.
|
||||
extension = "." + 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:
|
||||
Logger.log("d", "Writing to %s", file_name)
|
||||
|
|
|
@ -21,7 +21,7 @@ class RemovableDrivePlugin(OutputDevicePlugin):
|
|||
super().__init__()
|
||||
|
||||
self._update_thread = threading.Thread(target = self._updateThread)
|
||||
self._update_thread.deamon = True
|
||||
self._update_thread.daemon = True
|
||||
|
||||
self._check_updates = True
|
||||
|
||||
|
|
|
@ -286,9 +286,7 @@ Cura.ExpandableComponent
|
|||
UM.Label
|
||||
{
|
||||
text: label
|
||||
font: UM.Theme.getFont("default")
|
||||
elide: Text.ElideRight
|
||||
renderType: Text.NativeRendering
|
||||
color: UM.Theme.getColor("setting_control_text")
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: legendModelCheckBox.left
|
||||
|
|
|
@ -61,7 +61,7 @@ Window
|
|||
right: parent.right
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: headerText
|
||||
anchors
|
||||
|
@ -71,9 +71,7 @@ Window
|
|||
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:")
|
||||
color: UM.Theme.getColor("text")
|
||||
wrapMode: Text.WordWrap
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Cura.ScrollableTextArea
|
||||
|
|
|
@ -295,7 +295,6 @@ Cura.MachineAction
|
|||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@label", "Enter the IP address of your printer on the network.")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Cura.TextField
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import QtQuick 2.2
|
||||
import QtQuick.Controls 2.0
|
||||
import UM 1.3 as UM
|
||||
import UM 1.5 as UM
|
||||
|
||||
import Cura 1.6 as Cura
|
||||
|
||||
|
@ -57,32 +57,25 @@ Item
|
|||
width: Math.max(materialLabel.contentWidth, 60 * screenScaleFactor) // TODO: Theme!
|
||||
radius: 2 * screenScaleFactor // TODO: Theme!
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: materialLabel
|
||||
anchors.top: parent.top
|
||||
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
font: UM.Theme.getFont("default") // 12pt, regular
|
||||
text: ""
|
||||
visible: text !== ""
|
||||
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: printCoreLabel
|
||||
anchors.top: materialLabel.bottom
|
||||
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
font: UM.Theme.getFont("default_bold") // 12pt, bold
|
||||
text: ""
|
||||
visible: text !== ""
|
||||
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ Item
|
|||
property int size: 32 * screenScaleFactor // TODO: Theme!
|
||||
|
||||
// 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
|
||||
width: size
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import QtQuick 2.3
|
||||
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
|
||||
|
@ -31,7 +31,7 @@ Item
|
|||
id: contentWrapper
|
||||
implicitWidth: childrenRect.width
|
||||
implicitHeight: innerLabel.contentHeight + 2 * innerLabel.padding
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: innerLabel
|
||||
padding: 12 * screenScaleFactor // TODO: Theme!
|
||||
|
@ -39,7 +39,6 @@ Item
|
|||
wrapMode: Text.WordWrap
|
||||
width: 240 * screenScaleFactor // TODO: Theme!
|
||||
color: UM.Theme.getColor("monitor_tooltip_text")
|
||||
font: UM.Theme.getFont("default")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ Item
|
|||
anchors.centerIn: printJobPreview
|
||||
color: UM.Theme.getColor("monitor_placeholder_image")
|
||||
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
|
||||
not in order to determine if we show the placeholder (ultibot) image instead. */
|
||||
visible: printJob && previewImage.status == Image.Error
|
||||
|
|
|
@ -252,7 +252,7 @@ Item
|
|||
bottom: parent.bottom
|
||||
bottomMargin: 20 * screenScaleFactor // TODO: Theme!
|
||||
}
|
||||
iconSource: "../svg/icons/CameraPhoto.svg"
|
||||
iconSource: Qt.resolvedUrl("../svg/icons/CameraPhoto.svg")
|
||||
enabled: !cloudConnection
|
||||
visible: printer
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import QtQuick 2.2
|
|||
import QtQuick.Window 2.2
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
import UM 1.1 as UM
|
||||
import UM 1.5 as UM
|
||||
|
||||
UM.Dialog {
|
||||
|
||||
|
@ -70,7 +70,7 @@ UM.Dialog {
|
|||
name: "cura";
|
||||
}
|
||||
|
||||
Label {
|
||||
UM.Label {
|
||||
id: manualPrinterSelectionLabel;
|
||||
anchors {
|
||||
left: parent.left;
|
||||
|
@ -79,8 +79,6 @@ UM.Dialog {
|
|||
}
|
||||
height: 20 * screenScaleFactor;
|
||||
text: catalog.i18nc("@label", "Printer selection");
|
||||
wrapMode: Text.Wrap;
|
||||
renderType: Text.NativeRendering;
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
|
|
|
@ -236,6 +236,8 @@ class CloudOutputDeviceManager:
|
|||
)
|
||||
message.show()
|
||||
|
||||
new_devices_added = []
|
||||
|
||||
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.setText(message_text)
|
||||
|
@ -246,21 +248,25 @@ class CloudOutputDeviceManager:
|
|||
|
||||
# If there is no active machine, activate the first available cloud printer
|
||||
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)
|
||||
|
||||
max_disp_devices = 3
|
||||
if len(new_devices) > max_disp_devices:
|
||||
num_hidden = len(new_devices) - max_disp_devices
|
||||
if len(new_devices_added) > 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.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)
|
||||
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.setText(message_text)
|
||||
else:
|
||||
message.hide()
|
||||
|
||||
def _updateOnlinePrinters(self, printer_responses: Dict[str, CloudClusterResponse]) -> None:
|
||||
"""
|
||||
|
@ -385,23 +391,25 @@ class CloudOutputDeviceManager:
|
|||
if device.key in output_device_manager.getOutputDeviceIds():
|
||||
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]
|
||||
if not device:
|
||||
return
|
||||
return False
|
||||
|
||||
# Create a new machine.
|
||||
# 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:
|
||||
Logger.log("e", "Failed creating a new machine")
|
||||
return
|
||||
return False
|
||||
|
||||
self._setOutputDeviceMetadata(device, new_machine)
|
||||
|
||||
if activate:
|
||||
CuraApplication.getInstance().getMachineManager().setActiveMachine(new_machine.getId())
|
||||
|
||||
return True
|
||||
|
||||
def _connectToActiveMachine(self) -> None:
|
||||
"""Callback for when the active machine was changed by the user"""
|
||||
|
||||
|
@ -480,7 +488,7 @@ class CloudOutputDeviceManager:
|
|||
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?")
|
||||
result = QMessageBox.question(None, question_title, question_content)
|
||||
if result == QMessageBox.ButtonRole.NoRole:
|
||||
if result == QMessageBox.StandardButton.No:
|
||||
return
|
||||
|
||||
for machine_cloud_id in self.reported_device_ids:
|
||||
|
|
|
@ -169,7 +169,10 @@ class ClusterApiClient:
|
|||
"""
|
||||
|
||||
def parse() -> None:
|
||||
try:
|
||||
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
|
||||
if reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) is None:
|
||||
|
|
|
@ -5,7 +5,7 @@ import QtQuick 2.10
|
|||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import UM 1.3 as UM
|
||||
import UM 1.5 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
|
||||
|
@ -23,18 +23,16 @@ Cura.MachineAction
|
|||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: parent.width * 3 / 4
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: pageTitle
|
||||
width: parent.width
|
||||
text: catalog.i18nc("@title", "Build Plate Leveling")
|
||||
wrapMode: Text.WordWrap
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: pageDescription
|
||||
anchors.top: pageTitle.bottom
|
||||
|
@ -42,12 +40,9 @@ Cura.MachineAction
|
|||
width: parent.width
|
||||
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.")
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: bedlevelingText
|
||||
anchors.top: pageDescription.bottom
|
||||
|
@ -55,9 +50,6 @@ Cura.MachineAction
|
|||
width: parent.width
|
||||
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.")
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Row
|
||||
|
|
|
@ -20,7 +20,7 @@ Cura.MachineAction
|
|||
anchors.topMargin: UM.Theme.getSize("default_margin").width * 5
|
||||
anchors.leftMargin: UM.Theme.getSize("default_margin").width * 4
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: pageDescription
|
||||
anchors.top: parent.top
|
||||
|
@ -29,8 +29,6 @@ Cura.MachineAction
|
|||
wrapMode: Text.WordWrap
|
||||
text: catalog.i18nc("@label","Please select any upgrades made to this Ultimaker Original")
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
UM.CheckBox
|
||||
|
|
|
@ -343,6 +343,9 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
|
||||
return stream.getvalue().decode("utf-8")
|
||||
|
||||
def getFileName(self) -> str:
|
||||
return (self.getMetaDataEntry("base_file") + ".xml.fdm_material").replace(" ", "+")
|
||||
|
||||
# Recursively resolve loading inherited files
|
||||
def _resolveInheritance(self, file_name):
|
||||
xml = self._loadFile(file_name)
|
||||
|
@ -477,6 +480,15 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
|
||||
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):
|
||||
"""Overridden from InstanceContainer"""
|
||||
|
||||
|
|
|
@ -162,10 +162,6 @@
|
|||
{
|
||||
"default_value": true
|
||||
},
|
||||
"jerk_print":
|
||||
{
|
||||
"default_value": 12
|
||||
},
|
||||
"jerk_travel":
|
||||
{
|
||||
"value": "jerk_print if magic_spiralize else 20"
|
||||
|
|
|
@ -44,7 +44,6 @@
|
|||
"machine_max_jerk_xy": { "value": 8 },
|
||||
"machine_max_jerk_z": { "value": 0.4 },
|
||||
"machine_max_jerk_e": { "value": 5 },
|
||||
"machine_heated_bed": { "default_value": true },
|
||||
"material_diameter": { "default_value": 1.75 },
|
||||
"infill_overlap": { "default_value": 15 },
|
||||
"acceleration_print": { "value": 500 },
|
||||
|
|
|
@ -87,7 +87,6 @@
|
|||
"wall_0_wipe_dist": { "value": 0.2 },
|
||||
|
||||
"fill_outline_gaps": { "value": false },
|
||||
"filter_out_tiny_gaps": { "value": true },
|
||||
|
||||
"retraction_speed": {
|
||||
"maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')",
|
||||
|
|
|
@ -192,7 +192,6 @@
|
|||
"wall_0_wipe_dist": { "value": 0.0 },
|
||||
|
||||
"fill_outline_gaps": { "value": false },
|
||||
"filter_out_tiny_gaps": { "value": false },
|
||||
|
||||
"retraction_speed": {
|
||||
"maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')",
|
||||
|
|
|
@ -97,7 +97,6 @@
|
|||
"wall_0_wipe_dist": { "value": 0.0 },
|
||||
|
||||
"fill_outline_gaps": { "value": false },
|
||||
"filter_out_tiny_gaps": { "value": false },
|
||||
|
||||
"retraction_speed": {
|
||||
"maximum_value_warning": "machine_max_feedrate_e if retraction_enable else float('inf')",
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
"infill_overlap": { "value": 15.0 },
|
||||
"skin_overlap": { "value": 20.0 },
|
||||
"fill_outline_gaps": { "value": true },
|
||||
"filter_out_tiny_gaps": { "value": true },
|
||||
"roofing_layer_count": { "value": 2 },
|
||||
"xy_offset_layer_0": { "value": -0.1 },
|
||||
"speed_print": { "value": 50 },
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue