Merge branch 'Ultimaker:main' into master
|
@ -1,4 +0,0 @@
|
||||||
.git
|
|
||||||
.github
|
|
||||||
resources/materials
|
|
||||||
CuraEngine
|
|
4
.github/ISSUE_TEMPLATE/bugreport.yaml
vendored
|
@ -1,6 +1,6 @@
|
||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Create a report to help us fix issues.
|
description: Create a report to help us fix issues.
|
||||||
labels: "Type: Bug"
|
labels: ["Type: Bug", "Status: Triage"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
@ -14,7 +14,7 @@ body:
|
||||||
attributes:
|
attributes:
|
||||||
label: Application Version
|
label: Application Version
|
||||||
description: The version of Cura this issue occurs with.
|
description: The version of Cura this issue occurs with.
|
||||||
placeholder: 4.9.0
|
placeholder: 5.0.0
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
|
|
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.
|
|
21
.github/workflows/ci.yml
vendored
|
@ -1,21 +0,0 @@
|
||||||
---
|
|
||||||
name: CI
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- 'WIP**'
|
|
||||||
- '4.*'
|
|
||||||
- 'CURA-*'
|
|
||||||
pull_request:
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container: ultimaker/cura-build-environment
|
|
||||||
steps:
|
|
||||||
- name: Checkout Cura
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Build
|
|
||||||
run: docker/build.sh
|
|
||||||
- name: Test
|
|
||||||
run: docker/test.sh
|
|
144
.github/workflows/conan-package-create.yml
vendored
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
name: Create and Upload Conan package
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
recipe_id_full:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
recipe_id_latest:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
runs_on:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
python_version:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
conan_config_branch:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
conan_logging_level:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
conan_clean_local_cache:
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
conan_upload_community:
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
create_from_source:
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
conan-package-create:
|
||||||
|
runs-on: ${{ inputs.runs_on }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Python and pip
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: ${{ inputs.python_version }}
|
||||||
|
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 }}-create-cache
|
||||||
|
|
||||||
|
- 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 }}-create-cache
|
||||||
|
|
||||||
|
- 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 update
|
||||||
|
sudo apt upgrade
|
||||||
|
sudo apt install build-essential checkinstall libegl-dev 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 libxkbcommon-x11-dev pkg-config -y
|
||||||
|
|
||||||
|
- 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
|
||||||
|
if: ${{ !inputs.create_from_source }}
|
||||||
|
run: conan install ${{ inputs.recipe_id_full }} --build=missing --update
|
||||||
|
|
||||||
|
- name: Create the Packages (from source)
|
||||||
|
if: ${{ inputs.create_from_source }}
|
||||||
|
run: conan create . ${{ inputs.recipe_id_full }} --build=missing --update
|
||||||
|
|
||||||
|
- name: Remove the latest alias
|
||||||
|
if: ${{ inputs.create_from_source && inputs.recipe_id_latest != '' && runner.os == 'Linux' }}
|
||||||
|
run: |
|
||||||
|
conan remove ${{ inputs.recipe_id_latest }} -r cura -f || true
|
||||||
|
conan remove ${{ inputs.recipe_id_latest }} -r cura-ce -f || true
|
||||||
|
|
||||||
|
- name: Create the latest alias
|
||||||
|
if: ${{ inputs.create_from_source && inputs.recipe_id_latest != '' && always() }}
|
||||||
|
run: conan alias ${{ inputs.recipe_id_latest }} ${{ inputs.recipe_id_full }}
|
||||||
|
|
||||||
|
- name: Upload the Package(s)
|
||||||
|
if: always()
|
||||||
|
run: conan upload "*" -r cura --all -c
|
||||||
|
|
||||||
|
- name: Upload the Package(s) community
|
||||||
|
if: ${{ always() && inputs.conan_upload_community == 'true' }}
|
||||||
|
run: conan upload "*" -r cura-ce -c
|
103
.github/workflows/conan-package.yml
vendored
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
---
|
||||||
|
name: conan-package
|
||||||
|
|
||||||
|
# Exports the recipe, sources and binaries for Mac, Windows and Linux and upload these to the server such that these can
|
||||||
|
# be used downstream.
|
||||||
|
#
|
||||||
|
# It should run on pushes against main or CURA-* branches, but it will only create the binaries for main and release branches
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
create_binaries_windows:
|
||||||
|
required: true
|
||||||
|
default: false
|
||||||
|
description: 'create binaries Windows'
|
||||||
|
create_binaries_linux:
|
||||||
|
required: true
|
||||||
|
default: false
|
||||||
|
description: 'create binaries Linux'
|
||||||
|
create_binaries_macos:
|
||||||
|
required: true
|
||||||
|
default: false
|
||||||
|
description: 'create binaries Macos'
|
||||||
|
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'plugins/**'
|
||||||
|
- 'resources/**'
|
||||||
|
- 'cura/**'
|
||||||
|
- 'icons/**'
|
||||||
|
- 'tests/**'
|
||||||
|
- 'packaging/**'
|
||||||
|
- '.github/workflows/conan-*.yml'
|
||||||
|
- '.github/workflows/notify.yml'
|
||||||
|
- '.github/workflows/requirements-conan-package.txt'
|
||||||
|
- 'requirements*.txt'
|
||||||
|
- 'conanfile.py'
|
||||||
|
- 'conandata.yml'
|
||||||
|
- 'GitVersion.yml'
|
||||||
|
- '*.jinja'
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'CURA-*'
|
||||||
|
- '[1-9].[0-9]'
|
||||||
|
- '[1-9].[0-9][0-9]'
|
||||||
|
tags:
|
||||||
|
- '[1-9].[0-9].[0-9]+'
|
||||||
|
- '[1-9].[0-9][0-9].[0-9]+'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
conan-recipe-version:
|
||||||
|
uses: ultimaker/cura/.github/workflows/conan-recipe-version.yml@main
|
||||||
|
with:
|
||||||
|
project_name: cura
|
||||||
|
|
||||||
|
conan-package-export:
|
||||||
|
needs: [ conan-recipe-version ]
|
||||||
|
uses: ultimaker/cura/.github/workflows/conan-recipe-export.yml@main
|
||||||
|
with:
|
||||||
|
recipe_id_full: ${{ needs.conan-recipe-version.outputs.recipe_id_full }}
|
||||||
|
recipe_id_latest: ${{ needs.conan-recipe-version.outputs.recipe_id_latest }}
|
||||||
|
runs_on: 'ubuntu-20.04'
|
||||||
|
python_version: '3.10.x'
|
||||||
|
conan_logging_level: 'info'
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
conan-package-create-linux:
|
||||||
|
if: ${{ (github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || needs.conan-recipe-version.outputs.is_release_branch == 'true')) || (github.event_name == 'workflow_dispatch' && inputs.create_binaries_linux) }}
|
||||||
|
needs: [ conan-recipe-version, conan-package-export ]
|
||||||
|
|
||||||
|
uses: ultimaker/cura/.github/workflows/conan-package-create.yml@main
|
||||||
|
with:
|
||||||
|
recipe_id_full: ${{ needs.conan-recipe-version.outputs.recipe_id_full }}
|
||||||
|
runs_on: 'ubuntu-20.04'
|
||||||
|
python_version: '3.10.x'
|
||||||
|
conan_logging_level: 'info'
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
notify-export:
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs: [ conan-recipe-version, conan-package-export ]
|
||||||
|
|
||||||
|
uses: ultimaker/cura/.github/workflows/notify.yml@main
|
||||||
|
with:
|
||||||
|
success: ${{ contains(join(needs.*.result, ','), 'success') }}
|
||||||
|
success_title: "New Conan recipe exported in ${{ github.repository }}"
|
||||||
|
success_body: "Exported ${{ needs.conan-recipe-version.outputs.recipe_id_full }}"
|
||||||
|
failure_title: "Failed to export Conan Export in ${{ github.repository }}"
|
||||||
|
failure_body: "Failed to exported ${{ needs.conan-recipe-version.outputs.recipe_id_full }}"
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
notify-create:
|
||||||
|
if: ${{ always() && ((github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || needs.conan-recipe-version.outputs.is_release_branch == 'true')) || (github.event_name == 'workflow_dispatch' && inputs.create_binaries_linux)) }}
|
||||||
|
needs: [ conan-recipe-version, conan-package-create-linux ]
|
||||||
|
|
||||||
|
uses: ultimaker/cura/.github/workflows/notify.yml@main
|
||||||
|
with:
|
||||||
|
success: ${{ contains(join(needs.*.result, ','), 'success') }}
|
||||||
|
success_title: "New binaries created in ${{ github.repository }}"
|
||||||
|
success_body: "Created binaries for ${{ needs.conan-recipe-version.outputs.recipe_id_full }}"
|
||||||
|
failure_title: "Failed to create binaries in ${{ github.repository }}"
|
||||||
|
failure_body: "Failed to created binaries for ${{ needs.conan-recipe-version.outputs.recipe_id_full }}"
|
||||||
|
secrets: inherit
|
106
.github/workflows/conan-recipe-export.yml
vendored
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
name: Export Conan Recipe to server
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
recipe_id_full:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
recipe_id_latest:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
runs_on:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
python_version:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
conan_config_branch:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
conan_logging_level:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
conan_export_binaries:
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
conan_upload_community:
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
package-export:
|
||||||
|
runs-on: ${{ inputs.runs_on }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout project
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Python and pip
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: ${{ inputs.python_version }}
|
||||||
|
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: Cache Conan local repository packages
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: $HOME/.conan/data
|
||||||
|
key: ${{ runner.os }}-conan-export-cache
|
||||||
|
|
||||||
|
- 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: Export the Package (binaries)
|
||||||
|
if: ${{ inputs.conan_export_binaries }}
|
||||||
|
run: conan create . ${{ inputs.recipe_id_full }} --build=missing --update
|
||||||
|
|
||||||
|
- name: Export the Package
|
||||||
|
if: ${{ !inputs.conan_export_binaries }}
|
||||||
|
run: conan export . ${{ inputs.recipe_id_full }}
|
||||||
|
|
||||||
|
- name: Remove the latest alias
|
||||||
|
if: ${{ inputs.recipe_id_latest != '' && runner.os == 'Linux' }}
|
||||||
|
run: |
|
||||||
|
conan remove ${{ inputs.recipe_id_latest }} -r cura -f || true
|
||||||
|
conan remove ${{ inputs.recipe_id_latest }} -r cura-ce -f || true
|
||||||
|
|
||||||
|
- name: Create the latest alias
|
||||||
|
if: ${{ inputs.recipe_id_latest != '' && always() }}
|
||||||
|
run: conan alias ${{ inputs.recipe_id_latest }} ${{ inputs.recipe_id_full }}
|
||||||
|
|
||||||
|
- name: Upload the Package(s)
|
||||||
|
if: always()
|
||||||
|
run: conan upload "*" -r cura --all -c
|
||||||
|
|
||||||
|
- name: Upload the Package(s) community
|
||||||
|
if: ${{ always() && inputs.conan_upload_community == 'true' }}
|
||||||
|
run: conan upload "*" -r cura-ce -c
|
172
.github/workflows/conan-recipe-version.yml
vendored
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
name: Get Conan Recipe Version
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
project_name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
recipe_id_full:
|
||||||
|
description: "The full Conan recipe id: <name>/<version>@<user>/<channel>"
|
||||||
|
value: ${{ jobs.get-semver.outputs.recipe_id_full }}
|
||||||
|
|
||||||
|
recipe_id_latest:
|
||||||
|
description: "The full Conan recipe aliased (latest) id: <name>/(latest)@<user>/<channel>"
|
||||||
|
value: ${{ jobs.get-semver.outputs.recipe_id_latest }}
|
||||||
|
|
||||||
|
recipe_semver_full:
|
||||||
|
description: "The full semver <Major>.<Minor>.<Patch>-<PreReleaseTag>+<BuildMetaData>"
|
||||||
|
value: ${{ jobs.get-semver.outputs.semver_full }}
|
||||||
|
|
||||||
|
is_release_branch:
|
||||||
|
description: "is current branch a release branch?"
|
||||||
|
value: ${{ jobs.get-semver.outputs.release_branch }}
|
||||||
|
|
||||||
|
recipe_user:
|
||||||
|
description: "The conan user"
|
||||||
|
value: ${{ jobs.get-semver.outputs.user }}
|
||||||
|
|
||||||
|
recipe_channel:
|
||||||
|
description: "The conan channel"
|
||||||
|
value: ${{ jobs.get-semver.outputs.channel }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
get-semver:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
recipe_id_full: ${{ steps.get-conan-broadcast-data.outputs.recipe_id_full }}
|
||||||
|
recipe_id_latest: ${{ steps.get-conan-broadcast-data.outputs.recipe_id_latest }}
|
||||||
|
semver_full: ${{ steps.get-conan-broadcast-data.outputs.semver_full }}
|
||||||
|
is_release_branch: ${{ steps.get-conan-broadcast-data.outputs.is_release_branch }}
|
||||||
|
user: ${{ steps.get-conan-broadcast-data.outputs.user }}
|
||||||
|
channel: ${{ steps.get-conan-broadcast-data.outputs.channel }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ github.head_ref }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- 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
|
||||||
|
pip install gitpython
|
||||||
|
|
||||||
|
- id: get-conan-broadcast-data
|
||||||
|
name: Get Conan broadcast data
|
||||||
|
run: |
|
||||||
|
import subprocess
|
||||||
|
from conans import tools
|
||||||
|
from conans.errors import ConanException
|
||||||
|
from git import Repo
|
||||||
|
|
||||||
|
repo = Repo('.')
|
||||||
|
user = "${{ github.repository_owner }}".lower()
|
||||||
|
project_name = "${{ inputs.project_name }}"
|
||||||
|
event_name = "${{ github.event_name }}"
|
||||||
|
issue_number = "${{ github.ref }}".split('/')[2]
|
||||||
|
is_tag = "${{ github.ref_type }}" == "tag"
|
||||||
|
is_release_branch = False
|
||||||
|
|
||||||
|
# FIXME: for when we push a tag (such as an release)
|
||||||
|
channel = "testing"
|
||||||
|
if is_tag:
|
||||||
|
branch_version = tools.Version("${{ github.ref_name }}")
|
||||||
|
is_release_branch = True
|
||||||
|
channel = "_"
|
||||||
|
user = "_"
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
branch_version = tools.Version(repo.active_branch.name)
|
||||||
|
except ConanException:
|
||||||
|
branch_version = tools.Version('0.0.0')
|
||||||
|
if "${{ github.ref_name }}" == f"{branch_version.major}.{branch_version.minor}":
|
||||||
|
channel = 'stable'
|
||||||
|
is_release_branch = True
|
||||||
|
elif "${{ github.ref_name }}" in ("main", "master"):
|
||||||
|
channel = 'testing'
|
||||||
|
else:
|
||||||
|
channel = repo.active_branch.name.split("_")[0].replace("-", "_").lower()
|
||||||
|
|
||||||
|
if event_name == "pull_request":
|
||||||
|
channel = f"pr_{issue_number}"
|
||||||
|
|
||||||
|
# %% Get the actual version
|
||||||
|
latest_branch_version = tools.Version("0.0.0")
|
||||||
|
latest_branch_tag = None
|
||||||
|
for tag in repo.git.tag(merged = True).splitlines():
|
||||||
|
try:
|
||||||
|
version = tools.Version(tag)
|
||||||
|
except ConanException:
|
||||||
|
continue
|
||||||
|
if version > latest_branch_version:
|
||||||
|
latest_branch_version = version
|
||||||
|
latest_branch_tag = repo.tag(tag)
|
||||||
|
|
||||||
|
# %% Get the actual version
|
||||||
|
no_commits = 0
|
||||||
|
for commit in repo.iter_commits("HEAD"):
|
||||||
|
if commit == latest_branch_tag.commit:
|
||||||
|
break
|
||||||
|
no_commits += 1
|
||||||
|
|
||||||
|
if no_commits == 0:
|
||||||
|
# This is a release
|
||||||
|
actual_version = f"{latest_branch_version.major}.{latest_branch_version.minor}.{latest_branch_version.patch}"
|
||||||
|
if channel == "stable":
|
||||||
|
user = "_"
|
||||||
|
channel = "_"
|
||||||
|
else:
|
||||||
|
if event_name == "pull_request":
|
||||||
|
actual_version = f"{latest_branch_version.major}.{latest_branch_version.minor}.{latest_branch_version.patch}-{latest_branch_version.prerelease.lower()}+pr_{issue_number}_{no_commits}"
|
||||||
|
else:
|
||||||
|
if channel in ("stable", "_", ""):
|
||||||
|
channel_metadata = f"{no_commits}"
|
||||||
|
else:
|
||||||
|
channel_metadata = f"{channel}_{no_commits}"
|
||||||
|
# FIXME: for when we create a new release branch
|
||||||
|
if latest_branch_version.prerelease == "":
|
||||||
|
bump_up_minor = int(latest_branch_version.minor) + 1
|
||||||
|
actual_version = f"{latest_branch_version.major}.{bump_up_minor}.{latest_branch_version.patch}-alpha+{channel_metadata}"
|
||||||
|
else:
|
||||||
|
actual_version = f"{latest_branch_version.major}.{latest_branch_version.minor}.{latest_branch_version.patch}-{latest_branch_version.prerelease.lower()}+{channel_metadata}"
|
||||||
|
|
||||||
|
# %% print to output
|
||||||
|
cmd_name = ["echo", f"::set-output name=name::{project_name}"]
|
||||||
|
subprocess.call(cmd_name)
|
||||||
|
cmd_version = ["echo", f"::set-output name=version::{actual_version}"]
|
||||||
|
subprocess.call(cmd_version)
|
||||||
|
cmd_channel = ["echo", f"::set-output name=channel::{channel}"]
|
||||||
|
subprocess.call(cmd_channel)
|
||||||
|
cmd_id_full= ["echo", f"::set-output name=recipe_id_full::{project_name}/{actual_version}@{user}/{channel}"]
|
||||||
|
subprocess.call(cmd_id_full)
|
||||||
|
cmd_id_latest = ["echo", f"::set-output name=recipe_id_latest::{project_name}/latest@{user}/{channel}"]
|
||||||
|
subprocess.call(cmd_id_latest)
|
||||||
|
cmd_semver_full = ["echo", f"::set-output name=semver_full::{actual_version}"]
|
||||||
|
subprocess.call(cmd_semver_full)
|
||||||
|
cmd_is_release_branch = ["echo", f"::set-output name=is_release_branch::{str(is_release_branch).lower()}"]
|
||||||
|
subprocess.call(cmd_is_release_branch)
|
||||||
|
|
||||||
|
print("::group::Conan Recipe Information")
|
||||||
|
print(f"name = {project_name}")
|
||||||
|
print(f"version = {actual_version}")
|
||||||
|
print(f"user = {user}")
|
||||||
|
print(f"channel = {channel}")
|
||||||
|
print(f"recipe_id_full = {project_name}/{actual_version}@{user}/{channel}")
|
||||||
|
print(f"recipe_id_latest = {project_name}/latest@{user}/{channel}")
|
||||||
|
print(f"semver_full = {actual_version}")
|
||||||
|
print(f"is_release_branch = {str(is_release_branch).lower()}")
|
||||||
|
print("::endgroup::")
|
||||||
|
shell: python
|
245
.github/workflows/cura-installer.yml
vendored
Normal file
|
@ -0,0 +1,245 @@
|
||||||
|
name: Cura Installer
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
cura_conan_version:
|
||||||
|
description: 'Cura Conan Version'
|
||||||
|
default: 'cura/latest@ultimaker/testing'
|
||||||
|
required: true
|
||||||
|
conan_args:
|
||||||
|
description: 'Conan args: eq.: --require-override'
|
||||||
|
default: ''
|
||||||
|
required: false
|
||||||
|
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 3:25 UTC on working days
|
||||||
|
#FIXME: Provide the same default values as the workflow dispatch
|
||||||
|
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:
|
||||||
|
include:
|
||||||
|
- { os: macos-11, os_id: 'mac' }
|
||||||
|
- { os: windows-2022, os_id: 'win64' }
|
||||||
|
- { os: ubuntu-20.04, os_id: 'linux' }
|
||||||
|
- { os: ubuntu-22.04, os_id: 'linux-modern' }
|
||||||
|
|
||||||
|
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 }}-installer-cache
|
||||||
|
|
||||||
|
- 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 }}-installer-cache
|
||||||
|
|
||||||
|
- name: Install MacOS system requirements
|
||||||
|
if: ${{ runner.os == 'Macos' }}
|
||||||
|
run: brew install autoconf automake ninja create-dmg
|
||||||
|
|
||||||
|
- name: Install Linux system requirements
|
||||||
|
if: ${{ runner.os == 'Linux' }}
|
||||||
|
run: |
|
||||||
|
sudo apt update
|
||||||
|
sudo apt upgrade
|
||||||
|
sudo apt install build-essential checkinstall libegl-dev 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 libxkbcommon-x11-dev pkg-config -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)
|
||||||
|
id: macos-keychain
|
||||||
|
if: ${{ runner.os == 'Macos' }}
|
||||||
|
uses: apple-actions/import-codesign-certs@v1
|
||||||
|
with:
|
||||||
|
p12-file-base64: ${{ secrets.MACOS_CERT_P12 }}
|
||||||
|
p12-password: ${{ secrets.MACOS_CERT_PASSPHRASE }}
|
||||||
|
|
||||||
|
- 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 }} ${{ inputs.conan_args }} --build=missing --update -if cura_inst -g VirtualPythonEnv -o cura:enterprise=${{ inputs.enterprise }} -o cura:staging=${{ inputs.staging }} --json "cura_inst/conan_install_info.json"
|
||||||
|
|
||||||
|
- 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: Unlock Macos keychain (Bash)
|
||||||
|
if: ${{ runner.os == 'Macos' }}
|
||||||
|
run: security unlock -p $TEMP_KEYCHAIN_PASSWORD signing_temp.keychain
|
||||||
|
env:
|
||||||
|
TEMP_KEYCHAIN_PASSWORD: ${{ steps.macos-keychain.outputs.keychain-password }}
|
||||||
|
|
||||||
|
# FIXME: This is a workaround to ensure that we use and pack a shared library for OpenSSL 1.1.1l. We currently compile
|
||||||
|
# OpenSSL statically for CPython, but our Python Dependenies (such as PyQt6) require a shared library.
|
||||||
|
# Because Conan won't allow for building the same library with two different options (easily) we need to install it explicitly
|
||||||
|
# and do a manual copy to the VirtualEnv, such that Pyinstaller can find it.
|
||||||
|
|
||||||
|
- name: Install OpenSSL shared
|
||||||
|
run: conan install openssl/1.1.1l@_/_ --build=missing --update -o openssl:shared=True -g deploy
|
||||||
|
|
||||||
|
- name: Copy OpenSSL shared (Bash)
|
||||||
|
if: ${{ runner.os != 'Windows' }}
|
||||||
|
run: |
|
||||||
|
cp ./openssl/lib/*.so* ./cura_inst/bin/ || true
|
||||||
|
cp ./openssl/lib/*.dylib* ./cura_inst/bin/ || true
|
||||||
|
|
||||||
|
- name: Copy OpenSSL shared (Powershell)
|
||||||
|
if: ${{ runner.os == 'Windows' }}
|
||||||
|
run: |
|
||||||
|
cp openssl/bin/*.dll ./cura_inst/Scripts/
|
||||||
|
cp openssl/lib/*.lib ./cura_inst/Lib/
|
||||||
|
|
||||||
|
- 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-${{ matrix.os_id }}.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-${{ matrix.os_id }}.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\create_windows_installer.py ../cura_inst . "Ultimaker-Cura-$Env:CURA_VERSION_FULL-${{ matrix.os_id }}.exe"
|
||||||
|
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 "Ultimaker-Cura-$CURA_VERSION_FULL-${{ matrix.os_id }}.AppImage"
|
||||||
|
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_noterize.py ../cura_inst . "Ultimaker-Cura-$CURA_VERSION_FULL-${{ matrix.os_id }}.dmg"
|
||||||
|
working-directory: dist
|
||||||
|
|
||||||
|
- name: Upload the artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: Ultimaker-Cura-${{ env.CURA_VERSION_FULL }}-${{ matrix.os_id }}
|
||||||
|
path: |
|
||||||
|
dist/*.tar.gz
|
||||||
|
dist/*.zip
|
||||||
|
dist/*.exe
|
||||||
|
dist/*.msi
|
||||||
|
dist/*.dmg
|
||||||
|
dist/*.AppImage
|
||||||
|
dist/*.asc
|
||||||
|
retention-days: 5
|
||||||
|
|
||||||
|
notify-export:
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs: [ cura-installer-create ]
|
||||||
|
|
||||||
|
uses: ultimaker/cura/.github/workflows/notify.yml@main
|
||||||
|
with:
|
||||||
|
success: ${{ contains(join(needs.*.result, ','), 'success') }}
|
||||||
|
success_title: "Create the Cura distributions"
|
||||||
|
success_body: "Installers for ${{ inputs.cura_conan_version }}"
|
||||||
|
failure_title: "Failed to create the Cura distributions"
|
||||||
|
failure_body: "Failed to create at least 1 installer for ${{ inputs.cura_conan_version }}"
|
||||||
|
secrets: inherit
|
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: '* */12 * * *'
|
||||||
|
|
||||||
|
# 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.
|
54
.github/workflows/notify.yml
vendored
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
name: Get Conan Recipe Version
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
success:
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
success_title:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
success_body:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
failure_title:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
failure_body:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
slackNotification:
|
||||||
|
name: Slack Notification
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Slack notify on-success
|
||||||
|
if: ${{ inputs.success }}
|
||||||
|
uses: rtCamp/action-slack-notify@v2
|
||||||
|
env:
|
||||||
|
SLACK_USERNAME: ${{ github.repository }}
|
||||||
|
SLACK_COLOR: green
|
||||||
|
SLACK_ICON: https://github.com/Ultimaker/Cura/blob/main/icons/cura-128.png?raw=true
|
||||||
|
SLACK_TITLE: ${{ inputs.success_title }}
|
||||||
|
SLACK_MESSAGE: ${{ inputs.success_body }}
|
||||||
|
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||||
|
|
||||||
|
- name: Slack notify on-failure
|
||||||
|
if: ${{ !inputs.success }}
|
||||||
|
uses: rtCamp/action-slack-notify@v2
|
||||||
|
env:
|
||||||
|
SLACK_USERNAME: ${{ github.repository }}
|
||||||
|
SLACK_COLOR: red
|
||||||
|
SLACK_ICON: https://github.com/Ultimaker/Cura/blob/main/icons/cura-128.png?raw=true
|
||||||
|
SLACK_TITLE: ${{ inputs.failure_title }}
|
||||||
|
SLACK_MESSAGE: ${{ inputs.failure_body }}
|
||||||
|
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
2
.github/workflows/requirements-conan-package.txt
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
conan!=1.51.0
|
||||||
|
sip==6.5.1
|
164
.github/workflows/unit-test.yml
vendored
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
---
|
||||||
|
name: unit-test
|
||||||
|
# FIXME: This should be a reusable workflow
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'plugins/**'
|
||||||
|
- 'resources/**'
|
||||||
|
- 'cura/**'
|
||||||
|
- 'icons/**'
|
||||||
|
- 'tests/**'
|
||||||
|
- 'packaging/**'
|
||||||
|
- '.github/workflows/conan-*.yml'
|
||||||
|
- '.github/workflows/unit-test.yml'
|
||||||
|
- '.github/workflows/notify.yml'
|
||||||
|
- '.github/workflows/requirements-conan-package.txt'
|
||||||
|
- 'requirements*.txt'
|
||||||
|
- 'conanfile.py'
|
||||||
|
- 'conandata.yml'
|
||||||
|
- 'GitVersion.yml'
|
||||||
|
- '*.jinja'
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'CURA-*'
|
||||||
|
- '[1-9]+.[0-9]+'
|
||||||
|
tags:
|
||||||
|
- '[0-9]+.[0-9]+.[0-9]+'
|
||||||
|
- '[0-9]+.[0-9]+-beta'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'plugins/**'
|
||||||
|
- 'resources/**'
|
||||||
|
- 'cura/**'
|
||||||
|
- 'icons/**'
|
||||||
|
- 'tests/**'
|
||||||
|
- 'packaging/**'
|
||||||
|
- '.github/workflows/conan-*.yml'
|
||||||
|
- '.github/workflows/unit-test.yml'
|
||||||
|
- '.github/workflows/notify.yml'
|
||||||
|
- '.github/workflows/requirements-conan-package.txt'
|
||||||
|
- 'requirements*.txt'
|
||||||
|
- 'conanfile.py'
|
||||||
|
- 'conandata.yml'
|
||||||
|
- 'GitVersion.yml'
|
||||||
|
- '*.jinja'
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- '[1-9]+.[0-9]+'
|
||||||
|
tags:
|
||||||
|
- '[0-9]+.[0-9]+.[0-9]+'
|
||||||
|
- '[0-9]+.[0-9]+-beta'
|
||||||
|
|
||||||
|
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: info
|
||||||
|
CONAN_NON_INTERACTIVE: 1
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
conan-recipe-version:
|
||||||
|
uses: ultimaker/cura/.github/workflows/conan-recipe-version.yml@main
|
||||||
|
with:
|
||||||
|
project_name: cura
|
||||||
|
|
||||||
|
testing:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
needs: [ conan-recipe-version ]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Python and pip
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.10.x'
|
||||||
|
architecture: 'x64'
|
||||||
|
cache: 'pip'
|
||||||
|
cache-dependency-path: .github/workflows/requirements-conan-package.txt
|
||||||
|
|
||||||
|
- name: Install Python requirements and Create default Conan profile
|
||||||
|
run: |
|
||||||
|
pip install -r requirements-conan-package.txt
|
||||||
|
conan profile new default --detect
|
||||||
|
working-directory: .github/workflows/
|
||||||
|
|
||||||
|
- name: Use Conan download cache (Bash)
|
||||||
|
if: ${{ runner.os != 'Windows' }}
|
||||||
|
run: conan config set storage.download_cache="$HOME/.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 }}-unit-cache
|
||||||
|
|
||||||
|
- name: Install Linux system requirements
|
||||||
|
if: ${{ runner.os == 'Linux' }}
|
||||||
|
run: sudo apt install build-essential checkinstall libegl-dev 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 libxkbcommon-x11-dev -y
|
||||||
|
|
||||||
|
- name: Get Conan configuration
|
||||||
|
run: conan config install https://github.com/Ultimaker/conan-config.git
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: conan install . ${{ needs.conan-recipe-version.outputs.recipe_id_full }} --build=missing --update -o cura:devtools=True -g VirtualPythonEnv -if venv
|
||||||
|
|
||||||
|
- name: Upload the Dependency package(s)
|
||||||
|
run: conan upload "*" -r cura --all -c
|
||||||
|
|
||||||
|
- name: Set Environment variables for Cura (bash)
|
||||||
|
if: ${{ runner.os != 'Windows' }}
|
||||||
|
run: |
|
||||||
|
. ./venv/bin/activate_github_actions_env.sh
|
||||||
|
|
||||||
|
- name: Run Unit Test
|
||||||
|
id: run-test
|
||||||
|
run: |
|
||||||
|
pytest --junitxml=junit_cura.xml
|
||||||
|
working-directory: tests
|
||||||
|
|
||||||
|
- name: Upload Test Results
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: Test Results
|
||||||
|
path: "tests/**/*.xml"
|
||||||
|
|
||||||
|
publish-test-results:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
needs: [ testing ]
|
||||||
|
if: success() || failure()
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Python and pip
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.10.x'
|
||||||
|
architecture: 'x64'
|
||||||
|
cache: 'pip'
|
||||||
|
cache-dependency-path: .github/workflows/requirements-conan-package.txt
|
||||||
|
|
||||||
|
- name: Download Artifacts
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
path: artifacts
|
||||||
|
|
||||||
|
- name: Publish Unit Test Results
|
||||||
|
id: test-results
|
||||||
|
uses: EnricoMi/publish-unit-test-result-action@v1
|
||||||
|
with:
|
||||||
|
files: "artifacts/**/*.xml"
|
||||||
|
|
||||||
|
- name: Conclusion
|
||||||
|
run: echo "Conclusion is ${{ fromJSON( steps.test-results.outputs.json ).conclusion }}"
|
13
.gitignore
vendored
|
@ -38,6 +38,7 @@ cura.desktop
|
||||||
.settings
|
.settings
|
||||||
|
|
||||||
#Externally located plug-ins commonly installed by our devs.
|
#Externally located plug-ins commonly installed by our devs.
|
||||||
|
plugins/BarbarianPlugin
|
||||||
plugins/cura-big-flame-graph
|
plugins/cura-big-flame-graph
|
||||||
plugins/cura-camera-position
|
plugins/cura-camera-position
|
||||||
plugins/cura-god-mode-plugin
|
plugins/cura-god-mode-plugin
|
||||||
|
@ -64,6 +65,7 @@ plugins/CuraRemoteSupport
|
||||||
plugins/ModelCutter
|
plugins/ModelCutter
|
||||||
plugins/PrintProfileCreator
|
plugins/PrintProfileCreator
|
||||||
plugins/MultiPrintPlugin
|
plugins/MultiPrintPlugin
|
||||||
|
plugins/CuraOrientationPlugin
|
||||||
|
|
||||||
#Build stuff
|
#Build stuff
|
||||||
CMakeCache.txt
|
CMakeCache.txt
|
||||||
|
@ -87,4 +89,13 @@ CuraEngine
|
||||||
#Prevents import failures when plugin running tests
|
#Prevents import failures when plugin running tests
|
||||||
plugins/__init__.py
|
plugins/__init__.py
|
||||||
|
|
||||||
/venv
|
venv/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
conaninfo.txt
|
||||||
|
conan.lock
|
||||||
|
conan_imports_manifest.txt
|
||||||
|
conanbuildinfo.txt
|
||||||
|
graph_info.json
|
||||||
|
Ultimaker-Cura.spec
|
||||||
|
.run/
|
||||||
|
|
35
CITATION.cff
|
@ -1,11 +1,30 @@
|
||||||
# YAML 1.2
|
# YAML 1.2
|
||||||
---
|
---
|
||||||
authors:
|
cff-version: 1.2.0
|
||||||
cff-version: "1.1.0"
|
type: software
|
||||||
date-released: 2021-06-28
|
message: >-
|
||||||
license: "LGPL-3.0"
|
If you use this software, please cite it using the
|
||||||
message: "If you use this software, please cite it using these metadata."
|
metadata from this file.
|
||||||
repository-code: "https://github.com/ultimaker/cura/"
|
title: Ultimaker Cura
|
||||||
title: "Ultimaker Cura"
|
abstract: >-
|
||||||
version: "4.12.0"
|
A state-of-the-art slicer application built on top
|
||||||
|
of the Uranium framework.
|
||||||
|
authors:
|
||||||
|
- 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
|
||||||
...
|
...
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# Copyright (c) 2022 Ultimaker B.V.
|
# Copyright (c) 2022 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
# For MSVC flags, will be ignored on non-Windows OS's and this project in general. Only needed for cura-build-environment.
|
||||||
|
cmake_policy(SET CMP0091 NEW)
|
||||||
project(cura)
|
project(cura)
|
||||||
cmake_minimum_required(VERSION 3.18)
|
cmake_minimum_required(VERSION 3.18)
|
||||||
|
|
||||||
|
|
13
CuraVersion.py.jinja
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Copyright (c) 2022 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
CuraAppName = "{{ cura_app_name }}"
|
||||||
|
CuraAppDisplayName = "{{ cura_app_display_name }}"
|
||||||
|
CuraVersion = "{{ cura_version }}"
|
||||||
|
CuraBuildType = "{{ cura_build_type }}"
|
||||||
|
CuraDebugMode = {{ cura_debug_mode }}
|
||||||
|
CuraCloudAPIRoot = "{{ cura_cloud_api_root }}"
|
||||||
|
CuraCloudAPIVersion = "{{ cura_cloud_api_version }}"
|
||||||
|
CuraCloudAccountAPIRoot = "{{ cura_cloud_account_api_root }}"
|
||||||
|
CuraMarketplaceRoot = "{{ cura_marketplace_root }}"
|
||||||
|
CuraDigitalFactoryURL = "{{ cura_digital_factory_url }}"
|
45
Dockerfile
|
@ -1,45 +0,0 @@
|
||||||
FROM ultimaker/cura-build-environment:1
|
|
||||||
|
|
||||||
# Environment vars for easy configuration
|
|
||||||
ENV CURA_APP_DIR=/srv/cura
|
|
||||||
|
|
||||||
# Ensure our sources dir exists
|
|
||||||
RUN mkdir $CURA_APP_DIR
|
|
||||||
|
|
||||||
# Setup CuraEngine
|
|
||||||
ENV CURA_ENGINE_BRANCH=master
|
|
||||||
WORKDIR $CURA_APP_DIR
|
|
||||||
RUN git clone -b $CURA_ENGINE_BRANCH --depth 1 https://github.com/Ultimaker/CuraEngine
|
|
||||||
WORKDIR $CURA_APP_DIR/CuraEngine
|
|
||||||
RUN mkdir build
|
|
||||||
WORKDIR $CURA_APP_DIR/CuraEngine/build
|
|
||||||
RUN cmake3 ..
|
|
||||||
RUN make
|
|
||||||
RUN make install
|
|
||||||
|
|
||||||
# TODO: setup libCharon
|
|
||||||
|
|
||||||
# Setup Uranium
|
|
||||||
ENV URANIUM_BRANCH=master
|
|
||||||
WORKDIR $CURA_APP_DIR
|
|
||||||
RUN git clone -b $URANIUM_BRANCH --depth 1 https://github.com/Ultimaker/Uranium
|
|
||||||
|
|
||||||
# Setup materials
|
|
||||||
ENV MATERIALS_BRANCH=master
|
|
||||||
WORKDIR $CURA_APP_DIR
|
|
||||||
RUN git clone -b $MATERIALS_BRANCH --depth 1 https://github.com/Ultimaker/fdm_materials materials
|
|
||||||
|
|
||||||
# Setup Cura
|
|
||||||
WORKDIR $CURA_APP_DIR/Cura
|
|
||||||
ADD . .
|
|
||||||
RUN mv $CURA_APP_DIR/materials resources/materials
|
|
||||||
|
|
||||||
# Make sure Cura can find CuraEngine
|
|
||||||
RUN ln -s /usr/local/bin/CuraEngine $CURA_APP_DIR/Cura
|
|
||||||
|
|
||||||
# Run Cura
|
|
||||||
WORKDIR $CURA_APP_DIR/Cura
|
|
||||||
ENV PYTHONPATH=${PYTHONPATH}:$CURA_APP_DIR/Uranium
|
|
||||||
RUN chmod +x ./CuraEngine
|
|
||||||
RUN chmod +x ./run_in_docker.sh
|
|
||||||
CMD "./run_in_docker.sh"
|
|
55
GitVersion.yml
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
mode: ContinuousDelivery
|
||||||
|
next-version: 5.1
|
||||||
|
branches:
|
||||||
|
main:
|
||||||
|
regex: ^main$
|
||||||
|
mode: ContinuousDelivery
|
||||||
|
tag: alpha
|
||||||
|
increment: None
|
||||||
|
prevent-increment-of-merged-branch-version: true
|
||||||
|
track-merge-target: false
|
||||||
|
source-branches: [ ]
|
||||||
|
tracks-release-branches: false
|
||||||
|
is-release-branch: false
|
||||||
|
is-mainline: true
|
||||||
|
pre-release-weight: 55000
|
||||||
|
develop:
|
||||||
|
regex: ^CURA-.*$
|
||||||
|
mode: ContinuousDelivery
|
||||||
|
tag: alpha
|
||||||
|
increment: None
|
||||||
|
prevent-increment-of-merged-branch-version: false
|
||||||
|
track-merge-target: true
|
||||||
|
source-branches: [ 'main' ]
|
||||||
|
tracks-release-branches: true
|
||||||
|
is-release-branch: false
|
||||||
|
is-mainline: false
|
||||||
|
pre-release-weight: 0
|
||||||
|
release:
|
||||||
|
regex: ^[\d]+\.[\d]+$
|
||||||
|
mode: ContinuousDelivery
|
||||||
|
tag: beta
|
||||||
|
increment: None
|
||||||
|
prevent-increment-of-merged-branch-version: true
|
||||||
|
track-merge-target: false
|
||||||
|
source-branches: [ 'main' ]
|
||||||
|
tracks-release-branches: false
|
||||||
|
is-release-branch: true
|
||||||
|
is-mainline: false
|
||||||
|
pre-release-weight: 30000
|
||||||
|
pull-request-main:
|
||||||
|
regex: ^(pull|pull\-requests|pr)[/-]
|
||||||
|
mode: ContinuousDelivery
|
||||||
|
tag: alpha+
|
||||||
|
increment: Inherit
|
||||||
|
prevent-increment-of-merged-branch-version: true
|
||||||
|
tag-number-pattern: '[/-](?<number>\d+)[-/]'
|
||||||
|
track-merge-target: true
|
||||||
|
source-branches: [ 'main' ]
|
||||||
|
tracks-release-branches: false
|
||||||
|
is-release-branch: false
|
||||||
|
is-mainline: false
|
||||||
|
pre-release-weight: 30000
|
||||||
|
ignore:
|
||||||
|
sha: [ ]
|
||||||
|
merge-message-formats: { }
|
89
README.md
|
@ -1,61 +1,64 @@
|
||||||
Cura
|
# Cura
|
||||||
====
|
|
||||||
Ultimaker Cura is a state-of-the-art slicer application to prepare your 3D models for printing with a 3D printer. With hundreds of settings and hundreds of community-managed print profiles, Ultimaker Cura is sure to lead your next project to a success.
|
<p align="center">
|
||||||
|
<a href="https://github.com/Ultimaker/Cura/actions/workflows/unit-test.yml" alt="Unit Tests">
|
||||||
|
<img src="https://github.com/Ultimaker/Cura/actions/workflows/unit-test.yml/badge.svg" /></a>
|
||||||
|
<a href="https://github.com/Ultimaker/Cura/actions/workflows/conan-package.yml" alt="Unit Tests">
|
||||||
|
<img src="https://github.com/Ultimaker/Cura/actions/workflows/conan-package.yml/badge.svg" /></a>
|
||||||
|
<a href="https://github.com/Ultimaker/Cura/issues" alt="Open Issues">
|
||||||
|
<img src="https://img.shields.io/github/issues/ultimaker/cura" /></a>
|
||||||
|
<a href="https://github.com/Ultimaker/Cura/issues?q=is%3Aissue+is%3Aclosed" alt="Closed Issues">
|
||||||
|
<img src="https://img.shields.io/github/issues-closed/ultimaker/cura?color=g" /></a>
|
||||||
|
<a href="https://github.com/Ultimaker/Cura/pulls" alt="Pull Requests">
|
||||||
|
<img src="https://img.shields.io/github/issues-pr/ultimaker/cura" /></a>
|
||||||
|
<a href="https://github.com/Ultimaker/Cura/graphs/contributors" alt="Contributors">
|
||||||
|
<img src="https://img.shields.io/github/contributors/ultimaker/cura" /></a>
|
||||||
|
<a href="https://github.com/Ultimaker/Cura" alt="Repo Size">
|
||||||
|
<img src="https://img.shields.io/github/repo-size/ultimaker/cura?style=flat" /></a>
|
||||||
|
<a href="https://github.com/Ultimaker/Cura/blob/master/LICENSE" alt="License">
|
||||||
|
<img src="https://img.shields.io/github/license/ultimaker/cura?style=flat" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Ultimaker Cura is a state-of-the-art slicer application to prepare your 3D models for printing with a 3D printer. With hundreds of settings
|
||||||
|
and hundreds of community-managed print profiles, Ultimaker Cura is sure to lead your next project to a success.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Logging Issues
|
## Logging Issues
|
||||||
------------
|
|
||||||
For crashes and similar issues, please attach the following information:
|
For crashes and similar issues, please attach the following information:
|
||||||
|
|
||||||
* (On Windows) The log as produced by dxdiag (start -> run -> dxdiag -> save output)
|
* (On Windows) The log as produced by dxdiag (start -> run -> dxdiag -> save output)
|
||||||
* The Cura GUI log file, located at
|
* The Cura GUI log file, located at
|
||||||
* `%APPDATA%\cura\<Cura version>\cura.log` (Windows), or usually `C:\Users\<your username>\AppData\Roaming\cura\<Cura version>\cura.log`
|
* `%APPDATA%\cura\<Cura version>\cura.log` (Windows), or usually `C:\Users\<your username>\AppData\Roaming\cura\<Cura version>\cura.log`
|
||||||
* `$HOME/Library/Application Support/cura/<Cura version>/cura.log` (OSX)
|
* `$HOME/Library/Application Support/cura/<Cura version>/cura.log` (OSX)
|
||||||
* `$HOME/.local/share/cura/<Cura version>/cura.log` (Ubuntu/Linux)
|
* `$HOME/.local/share/cura/<Cura version>/cura.log` (Ubuntu/Linux)
|
||||||
|
|
||||||
If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder
|
If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder.
|
||||||
|
An alternative is to install the [ExtensiveSupportLogging plugin](https://marketplace.ultimaker.com/app/cura/plugins/UltimakerPackages/ExtensiveSupportLogging)
|
||||||
|
this creates a zip folder of the relevant log files. If you're experiencing performance issues, we might ask you to connect the CPU profiler
|
||||||
|
in this plugin and attach the collected data to your support ticket.
|
||||||
|
|
||||||
For additional support, you could also ask in the [#cura channel](https://web.libera.chat/#cura) on [libera.chat](https://libera.chat/). For help with development, there is also the [#cura-dev channel](https://web.libera.chat/#cura-dev).
|
## Running from Source
|
||||||
|
|
||||||
Dependencies
|
|
||||||
------------
|
|
||||||
* [Uranium](https://github.com/Ultimaker/Uranium) Cura is built on top of the Uranium framework.
|
|
||||||
* [CuraEngine](https://github.com/Ultimaker/CuraEngine) This will be needed at runtime to perform the actual slicing.
|
|
||||||
* [fdm_materials](https://github.com/Ultimaker/fdm_materials) Required to load a printer that has swappable material profiles.
|
|
||||||
* [PySerial](https://github.com/pyserial/pyserial) Only required for USB printing support.
|
|
||||||
* [python-zeroconf](https://github.com/jstasiak/python-zeroconf) Only required to detect mDNS-enabled printers.
|
|
||||||
|
|
||||||
For a list of required Python packages, with their recommended version, see `requirements.txt`.
|
|
||||||
|
|
||||||
This list is not exhaustive at the moment, please check the links in the next section for more details.
|
|
||||||
|
|
||||||
Build scripts
|
|
||||||
-------------
|
|
||||||
Please check out [cura-build](https://github.com/Ultimaker/cura-build) for detailed building instructions.
|
|
||||||
|
|
||||||
If you want to build the entire environment from scratch before building Cura as well, [cura-build-environment](https://github.com/Ultimaker/cura-build-environment) might be a starting point before cura-build. (Again, see cura-build for more details.)
|
|
||||||
|
|
||||||
Running from Source
|
|
||||||
-------------
|
|
||||||
Please check our [Wiki page](https://github.com/Ultimaker/Cura/wiki/Running-Cura-from-Source) for details about running Cura from source.
|
Please check our [Wiki page](https://github.com/Ultimaker/Cura/wiki/Running-Cura-from-Source) for details about running Cura from source.
|
||||||
|
|
||||||
Plugins
|
## Plugins
|
||||||
-------------
|
|
||||||
Please check our [Wiki page](https://github.com/Ultimaker/Cura/wiki/Plugin-Directory) for details about creating and using plugins.
|
Please check our [Wiki page](https://github.com/Ultimaker/Cura/wiki/Plugin-Directory) for details about creating and using plugins.
|
||||||
|
|
||||||
Supported printers
|
## Supported printers
|
||||||
-------------
|
Please check our [Wiki page](https://github.com/Ultimaker/Cura/wiki/Adding-new-machine-profiles-to-Cura) for guidelines about adding support
|
||||||
Please check our [Wiki page](https://github.com/Ultimaker/Cura/wiki/Adding-new-machine-profiles-to-Cura) for guidelines about adding support for new machines.
|
for new machines.
|
||||||
|
|
||||||
Configuring Cura
|
## Configuring Cura
|
||||||
----------------
|
|
||||||
Please check out [Wiki page](https://github.com/Ultimaker/Cura/wiki/Cura-Settings) about configuration options for developers.
|
Please check out [Wiki page](https://github.com/Ultimaker/Cura/wiki/Cura-Settings) about configuration options for developers.
|
||||||
|
|
||||||
Translating Cura
|
## Translating Cura
|
||||||
----------------
|
|
||||||
Please check out [Wiki page](https://github.com/Ultimaker/Cura/wiki/Translating-Cura) about how to translate Cura into other languages.
|
Please check out [Wiki page](https://github.com/Ultimaker/Cura/wiki/Translating-Cura) about how to translate Cura into other languages.
|
||||||
|
|
||||||
License
|
## License
|
||||||
----------------
|

|
||||||
Cura is released under the terms of the LGPLv3 or higher. A copy of this license should be included with the software.
|
Cura is released under terms of the LGPLv3 or higher. A copy of this license should be included with the software. Terms of the license can be found in the LICENSE file. Or at
|
||||||
|
http://www.gnu.org/licenses/lgpl.html
|
||||||
|
|
||||||
|
> But in general it boils down to:
|
||||||
|
> **You need to share the source of any Cura modifications**
|
||||||
|
|
273
Ultimaker-Cura.spec.jinja
Normal file
|
@ -0,0 +1,273 @@
|
||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from PyInstaller.utils.hooks import collect_all
|
||||||
|
|
||||||
|
|
||||||
|
datas = {{ datas }}
|
||||||
|
binaries = {{ binaries }}
|
||||||
|
|
||||||
|
hiddenimports = {{ hiddenimports }}
|
||||||
|
|
||||||
|
{% for value in collect_all %}tmp_ret = collect_all('{{ value }}')
|
||||||
|
datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
# Add dynamic libs in the venv bin/Script Path. This is needed because we might copy some additional libs
|
||||||
|
# e.q.: OpenSSL 1.1.1l in that directory with a separate:
|
||||||
|
# `conan install openssl@1.1.1l -g deploy && cp openssl/bin/*.so cura_inst/bin`
|
||||||
|
binaries.extend([(str(bin), ".") for bin in Path("{{ venv_script_path }}").glob("*.so*")])
|
||||||
|
binaries.extend([(str(bin), ".") for bin in Path("{{ venv_script_path }}").glob("*.dll")])
|
||||||
|
binaries.extend([(str(bin), ".") for bin in Path("{{ venv_script_path }}").glob("*.dylib")])
|
||||||
|
|
||||||
|
block_cipher = None
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
[{{ entrypoint }}],
|
||||||
|
pathex=[],
|
||||||
|
binaries=binaries,
|
||||||
|
datas=datas,
|
||||||
|
hiddenimports=hiddenimports,
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
win_no_prefer_redirects=False,
|
||||||
|
win_private_assemblies=False,
|
||||||
|
cipher=block_cipher,
|
||||||
|
noarchive=False
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
[],
|
||||||
|
exclude_binaries=True,
|
||||||
|
name=r'{{ name }}',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip={{ strip }},
|
||||||
|
upx={{ upx }},
|
||||||
|
console=False,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch={{ target_arch }},
|
||||||
|
codesign_identity=os.getenv('CODESIGN_IDENTITY', None),
|
||||||
|
entitlements_file={{ entitlements_file }},
|
||||||
|
icon={{ icon }}
|
||||||
|
)
|
||||||
|
|
||||||
|
coll = COLLECT(
|
||||||
|
exe,
|
||||||
|
a.binaries,
|
||||||
|
a.zipfiles,
|
||||||
|
a.datas,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
name=r'{{ name }}'
|
||||||
|
)
|
||||||
|
|
||||||
|
{% if macos == true %}
|
||||||
|
# PyInstaller seems to copy everything in the resource folder for the MacOS, this causes issues with codesigning and notarizing
|
||||||
|
# The folder structure should adhere to the one specified in Table 2-5
|
||||||
|
# https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW1
|
||||||
|
# The class below is basically ducktyping the BUNDLE class of PyInstaller and using our own `assemble` method for more fine-grain and specific
|
||||||
|
# control. Some code of the method below is copied from:
|
||||||
|
# https://github.com/pyinstaller/pyinstaller/blob/22d1d2a5378228744cc95f14904dae1664df32c4/PyInstaller/building/osx.py#L115
|
||||||
|
#-----------------------------------------------------------------------------
|
||||||
|
# Copyright (c) 2005-2022, PyInstaller Development Team.
|
||||||
|
#
|
||||||
|
# Distributed under the terms of the GNU General Public License (version 2
|
||||||
|
# or later) with exception for distributing the bootloader.
|
||||||
|
#
|
||||||
|
# The full license is in the file COPYING.txt, distributed with this software.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
|
||||||
|
#-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import plistlib
|
||||||
|
import shutil
|
||||||
|
import PyInstaller.utils.osx as osxutils
|
||||||
|
from pathlib import Path
|
||||||
|
from PyInstaller.building.osx import BUNDLE
|
||||||
|
from PyInstaller.building.utils import (_check_path_overlap, _rmtree, add_suffix_to_extension, checkCache)
|
||||||
|
from PyInstaller.building.datastruct import logger
|
||||||
|
from PyInstaller.building.icon import normalize_icon_type
|
||||||
|
|
||||||
|
|
||||||
|
class UMBUNDLE(BUNDLE):
|
||||||
|
def assemble(self):
|
||||||
|
from PyInstaller.config import CONF
|
||||||
|
|
||||||
|
if _check_path_overlap(self.name) and os.path.isdir(self.name):
|
||||||
|
_rmtree(self.name)
|
||||||
|
logger.info("Building BUNDLE %s", self.tocbasename)
|
||||||
|
|
||||||
|
# Create a minimal Mac bundle structure.
|
||||||
|
macos_path = Path(self.name, "Contents", "MacOS")
|
||||||
|
resources_path = Path(self.name, "Contents", "Resources")
|
||||||
|
frameworks_path = Path(self.name, "Contents", "Frameworks")
|
||||||
|
os.makedirs(macos_path)
|
||||||
|
os.makedirs(resources_path)
|
||||||
|
os.makedirs(frameworks_path)
|
||||||
|
|
||||||
|
# Makes sure the icon exists and attempts to convert to the proper format if applicable
|
||||||
|
self.icon = normalize_icon_type(self.icon, ("icns",), "icns", CONF["workpath"])
|
||||||
|
|
||||||
|
# Ensure icon path is absolute
|
||||||
|
self.icon = os.path.abspath(self.icon)
|
||||||
|
|
||||||
|
# Copy icns icon to Resources directory.
|
||||||
|
shutil.copy(self.icon, os.path.join(self.name, 'Contents', 'Resources'))
|
||||||
|
|
||||||
|
# Key/values for a minimal Info.plist file
|
||||||
|
info_plist_dict = {
|
||||||
|
"CFBundleDisplayName": self.appname,
|
||||||
|
"CFBundleName": self.appname,
|
||||||
|
|
||||||
|
# Required by 'codesign' utility.
|
||||||
|
# The value for CFBundleIdentifier is used as the default unique name of your program for Code Signing
|
||||||
|
# purposes. It even identifies the APP for access to restricted OS X areas like Keychain.
|
||||||
|
#
|
||||||
|
# The identifier used for signing must be globally unique. The usual form for this identifier is a
|
||||||
|
# hierarchical name in reverse DNS notation, starting with the toplevel domain, followed by the company
|
||||||
|
# name, followed by the department within the company, and ending with the product name. Usually in the
|
||||||
|
# form: com.mycompany.department.appname
|
||||||
|
# CLI option --osx-bundle-identifier sets this value.
|
||||||
|
"CFBundleIdentifier": self.bundle_identifier,
|
||||||
|
"CFBundleExecutable": os.path.basename(self.exename),
|
||||||
|
"CFBundleIconFile": os.path.basename(self.icon),
|
||||||
|
"CFBundleInfoDictionaryVersion": "6.0",
|
||||||
|
"CFBundlePackageType": "APPL",
|
||||||
|
"CFBundleShortVersionString": self.version,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set some default values. But they still can be overwritten by the user.
|
||||||
|
if self.console:
|
||||||
|
# Setting EXE console=True implies LSBackgroundOnly=True.
|
||||||
|
info_plist_dict['LSBackgroundOnly'] = True
|
||||||
|
else:
|
||||||
|
# Let's use high resolution by default.
|
||||||
|
info_plist_dict['NSHighResolutionCapable'] = True
|
||||||
|
|
||||||
|
# Merge info_plist settings from spec file
|
||||||
|
if isinstance(self.info_plist, dict) and self.info_plist:
|
||||||
|
info_plist_dict.update(self.info_plist)
|
||||||
|
|
||||||
|
plist_filename = os.path.join(self.name, "Contents", "Info.plist")
|
||||||
|
with open(plist_filename, "wb") as plist_fh:
|
||||||
|
plistlib.dump(info_plist_dict, plist_fh)
|
||||||
|
|
||||||
|
links = []
|
||||||
|
_QT_BASE_PATH = {'PySide2', 'PySide6', 'PyQt5', 'PyQt6', 'PySide6'}
|
||||||
|
for inm, fnm, typ in self.toc:
|
||||||
|
# Adjust name for extensions, if applicable
|
||||||
|
inm, fnm, typ = add_suffix_to_extension(inm, fnm, typ)
|
||||||
|
inm = Path(inm)
|
||||||
|
fnm = Path(fnm)
|
||||||
|
# Copy files from cache. This ensures that are used files with relative paths to dynamic library
|
||||||
|
# dependencies (@executable_path)
|
||||||
|
if typ in ('EXTENSION', 'BINARY') or (typ == 'DATA' and inm.suffix == '.so'):
|
||||||
|
if any(['.' in p for p in inm.parent.parts]):
|
||||||
|
inm = Path(inm.name)
|
||||||
|
fnm = Path(checkCache(
|
||||||
|
str(fnm),
|
||||||
|
strip = self.strip,
|
||||||
|
upx = self.upx,
|
||||||
|
upx_exclude = self.upx_exclude,
|
||||||
|
dist_nm = str(inm),
|
||||||
|
target_arch = self.target_arch,
|
||||||
|
codesign_identity = self.codesign_identity,
|
||||||
|
entitlements_file = self.entitlements_file,
|
||||||
|
strict_arch_validation = (typ == 'EXTENSION'),
|
||||||
|
))
|
||||||
|
frame_dst = frameworks_path.joinpath(inm)
|
||||||
|
if not frame_dst.exists():
|
||||||
|
if frame_dst.is_dir():
|
||||||
|
os.makedirs(frame_dst, exist_ok = True)
|
||||||
|
else:
|
||||||
|
os.makedirs(frame_dst.parent, exist_ok = True)
|
||||||
|
shutil.copy(fnm, frame_dst, follow_symlinks = True)
|
||||||
|
macos_dst = macos_path.joinpath(inm)
|
||||||
|
if not macos_dst.exists():
|
||||||
|
if macos_dst.is_dir():
|
||||||
|
os.makedirs(macos_dst, exist_ok = True)
|
||||||
|
else:
|
||||||
|
os.makedirs(macos_dst.parent, exist_ok = True)
|
||||||
|
|
||||||
|
# Create relative symlink to the framework
|
||||||
|
symlink_to = Path(*[".." for p in macos_dst.relative_to(macos_path).parts], "Frameworks").joinpath(
|
||||||
|
frame_dst.relative_to(frameworks_path))
|
||||||
|
try:
|
||||||
|
macos_dst.symlink_to(symlink_to)
|
||||||
|
except FileExistsError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if typ == 'DATA':
|
||||||
|
if any(['.' in p for p in inm.parent.parts]) or inm.suffix == '.so':
|
||||||
|
# Skip info dist egg and some not needed folders in tcl and tk, since they all contain dots in their files
|
||||||
|
logger.warning(f"Skipping DATA file {inm}")
|
||||||
|
continue
|
||||||
|
res_dst = resources_path.joinpath(inm)
|
||||||
|
if not res_dst.exists():
|
||||||
|
if res_dst.is_dir():
|
||||||
|
os.makedirs(res_dst, exist_ok = True)
|
||||||
|
else:
|
||||||
|
os.makedirs(res_dst.parent, exist_ok = True)
|
||||||
|
shutil.copy(fnm, res_dst, follow_symlinks = True)
|
||||||
|
macos_dst = macos_path.joinpath(inm)
|
||||||
|
if not macos_dst.exists():
|
||||||
|
if macos_dst.is_dir():
|
||||||
|
os.makedirs(macos_dst, exist_ok = True)
|
||||||
|
else:
|
||||||
|
os.makedirs(macos_dst.parent, exist_ok = True)
|
||||||
|
|
||||||
|
# Create relative symlink to the resource
|
||||||
|
symlink_to = Path(*[".." for p in macos_dst.relative_to(macos_path).parts], "Resources").joinpath(
|
||||||
|
res_dst.relative_to(resources_path))
|
||||||
|
try:
|
||||||
|
macos_dst.symlink_to(symlink_to)
|
||||||
|
except FileExistsError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
macos_dst = macos_path.joinpath(inm)
|
||||||
|
if not macos_dst.exists():
|
||||||
|
if macos_dst.is_dir():
|
||||||
|
os.makedirs(macos_dst, exist_ok = True)
|
||||||
|
else:
|
||||||
|
os.makedirs(macos_dst.parent, exist_ok = True)
|
||||||
|
shutil.copy(fnm, macos_dst, follow_symlinks = True)
|
||||||
|
|
||||||
|
# Sign the bundle
|
||||||
|
logger.info('Signing the BUNDLE...')
|
||||||
|
try:
|
||||||
|
osxutils.sign_binary(self.name, self.codesign_identity, self.entitlements_file, deep = True)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error while signing the bundle: {e}")
|
||||||
|
logger.warning("You will need to sign the bundle manually!")
|
||||||
|
|
||||||
|
logger.info(f"Building BUNDLE {self.tocbasename} completed successfully.")
|
||||||
|
|
||||||
|
app = UMBUNDLE(
|
||||||
|
coll,
|
||||||
|
name='{{ name }}.app',
|
||||||
|
icon={{ icon }},
|
||||||
|
bundle_identifier={{ osx_bundle_identifier }},
|
||||||
|
version={{ version }},
|
||||||
|
info_plist={
|
||||||
|
'CFBundleDisplayName': '{{ display_name }}',
|
||||||
|
'NSPrincipalClass': 'NSApplication',
|
||||||
|
'CFBundleDevelopmentRegion': 'English',
|
||||||
|
'CFBundleExecutable': '{{ name }}',
|
||||||
|
'CFBundleInfoDictionaryVersion': '6.0',
|
||||||
|
'CFBundlePackageType': 'APPL',
|
||||||
|
'CFBundleShortVersionString': {{ short_version }},
|
||||||
|
'CFBundleDocumentTypes': [{
|
||||||
|
'CFBundleTypeRole': 'Viewer',
|
||||||
|
'CFBundleTypeExtensions': ['*'],
|
||||||
|
'CFBundleTypeName': 'Model Files',
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
){% endif %}
|
34
build.sh
|
@ -1,34 +0,0 @@
|
||||||
set -e
|
|
||||||
set -u
|
|
||||||
|
|
||||||
export OLD_PWD=`pwd`
|
|
||||||
export CMAKE=/c/software/PCL/cmake-3.0.1-win32-x86/bin/cmake.exe
|
|
||||||
export MAKE=mingw32-make.exe
|
|
||||||
export PATH=/c/mingw-w64/i686-4.9.2-posix-dwarf-rt_v3-rev1/mingw32/bin:$PATH
|
|
||||||
|
|
||||||
mkdir -p /c/software/protobuf/_build
|
|
||||||
cd /c/software/protobuf/_build
|
|
||||||
$CMAKE ../
|
|
||||||
$MAKE install
|
|
||||||
|
|
||||||
mkdir -p /c/software/libArcus/_build
|
|
||||||
cd /c/software/libArcus/_build
|
|
||||||
$CMAKE ../
|
|
||||||
$MAKE install
|
|
||||||
|
|
||||||
mkdir -p /c/software/PinkUnicornEngine/_build
|
|
||||||
cd /c/software/PinkUnicornEngine/_build
|
|
||||||
$CMAKE ../
|
|
||||||
$MAKE
|
|
||||||
|
|
||||||
cd $OLD_PWD
|
|
||||||
export PYTHONPATH=`pwd`/../libArcus/python:/c/Software/Uranium/
|
|
||||||
/c/python34/python setup.py py2exe
|
|
||||||
|
|
||||||
cp /c/software/PinkUnicornEngine/_build/CuraEngine.exe dist/
|
|
||||||
cp /c/software/libArcus/_install/bin/libArcus.dll dist/
|
|
||||||
cp /c/mingw-w64/i686-4.9.2-posix-dwarf-rt_v3-rev1/mingw32/bin/libgcc_s_dw2-1.dll dist/
|
|
||||||
cp /c/mingw-w64/i686-4.9.2-posix-dwarf-rt_v3-rev1/mingw32/bin/libwinpthread-1.dll dist/
|
|
||||||
cp /c/mingw-w64/i686-4.9.2-posix-dwarf-rt_v3-rev1/mingw32/bin/libstdc++-6.dll dist/
|
|
||||||
|
|
||||||
/c/program\ files\ \(x86\)/NSIS/makensis.exe installer.nsi
|
|
|
@ -25,9 +25,10 @@
|
||||||
</description>
|
</description>
|
||||||
<screenshots>
|
<screenshots>
|
||||||
<screenshot type="default">
|
<screenshot type="default">
|
||||||
<image>https://raw.githubusercontent.com/Ultimaker/Cura/master/screenshot.png</image>
|
<image>https://raw.githubusercontent.com/Ultimaker/Cura/main/cura-logo.PNG</image>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
</screenshots>
|
</screenshots>
|
||||||
<url type="homepage">https://ultimaker.com/software/ultimaker-cura?utm_source=cura&utm_medium=software&utm_campaign=cura-update-linux</url>
|
<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>
|
<translation type="gettext">Cura</translation>
|
||||||
|
<content_rating type="oars-1.1" />
|
||||||
</component>
|
</component>
|
||||||
|
|
288
conandata.yml
Normal file
|
@ -0,0 +1,288 @@
|
||||||
|
---
|
||||||
|
# Usage: defaults to None
|
||||||
|
# If you're on a release branch create an entry for that **version** e.q.: `5.1.0` update the requirements (use pinned versions, not latest)
|
||||||
|
# also create a beta entry for that **version** e.q.: `5.1.0-beta`, update the requirements (use the <dep_name>/(latest)@ultimaker/stable)
|
||||||
|
#
|
||||||
|
# If you're working on a feature/bugfix branch from a release branch, create an entry for that **channel**, update the requirements (use
|
||||||
|
# the <dep_name>/(latest)@ultimaker/stable)
|
||||||
|
#
|
||||||
|
# If you're working on a feature/bugfix branch from a main branch, it is optional to create an entry for that **channel**, update the
|
||||||
|
# requirements (use the <dep_name>/(latest)@ultimaker/testing)
|
||||||
|
#
|
||||||
|
# Subject to change in the future!
|
||||||
|
"None":
|
||||||
|
requirements:
|
||||||
|
- "pyarcus/(latest)@ultimaker/testing"
|
||||||
|
- "curaengine/(latest)@ultimaker/testing"
|
||||||
|
- "pysavitar/(latest)@ultimaker/testing"
|
||||||
|
- "pynest2d/(latest)@ultimaker/testing"
|
||||||
|
- "uranium/(latest)@ultimaker/testing"
|
||||||
|
- "fdm_materials/(latest)@ultimaker/testing"
|
||||||
|
- "cura_binary_data/(latest)@ultimaker/testing"
|
||||||
|
- "cpython/3.10.4"
|
||||||
|
runinfo:
|
||||||
|
entrypoint: "cura_app.py"
|
||||||
|
pyinstaller:
|
||||||
|
datas:
|
||||||
|
cura_plugins:
|
||||||
|
package: "cura"
|
||||||
|
src: "plugins"
|
||||||
|
dst: "share/cura/plugins"
|
||||||
|
cura_resources:
|
||||||
|
package: "cura"
|
||||||
|
src: "resources"
|
||||||
|
dst: "share/cura/resources"
|
||||||
|
uranium_plugins:
|
||||||
|
package: "uranium"
|
||||||
|
src: "plugins"
|
||||||
|
dst: "share/uranium/plugins"
|
||||||
|
uranium_resources:
|
||||||
|
package: "uranium"
|
||||||
|
src: "resources"
|
||||||
|
dst: "share/uranium/resources"
|
||||||
|
uranium_um_qt_qml_um:
|
||||||
|
package: "uranium"
|
||||||
|
src: "site-packages/UM/Qt/qml/UM"
|
||||||
|
dst: "PyQt6/Qt6/qml/UM"
|
||||||
|
cura_binary_data:
|
||||||
|
package: "cura_binary_data"
|
||||||
|
src: "resources/cura/resources"
|
||||||
|
dst: "share/cura/resources"
|
||||||
|
uranium_binary_data:
|
||||||
|
package: "cura_binary_data"
|
||||||
|
src: "resources/uranium/resources"
|
||||||
|
dst: "share/uranium/resources"
|
||||||
|
windows_binary_data:
|
||||||
|
package: "cura_binary_data"
|
||||||
|
src: "windows"
|
||||||
|
dst: "share/windows"
|
||||||
|
fdm_materials:
|
||||||
|
package: "fdm_materials"
|
||||||
|
src: "materials"
|
||||||
|
dst: "share/cura/resources/materials"
|
||||||
|
tcl:
|
||||||
|
package: "tcl"
|
||||||
|
src: "lib/tcl8.6"
|
||||||
|
dst: "tcl"
|
||||||
|
tk:
|
||||||
|
package: "tk"
|
||||||
|
src: "lib/tk8.6"
|
||||||
|
dst: "tk"
|
||||||
|
binaries:
|
||||||
|
curaengine:
|
||||||
|
package: "curaengine"
|
||||||
|
src: "bin"
|
||||||
|
dst: "."
|
||||||
|
binary: "CuraEngine"
|
||||||
|
hiddenimports:
|
||||||
|
- "pySavitar"
|
||||||
|
- "pyArcus"
|
||||||
|
- "pynest2d"
|
||||||
|
- "PyQt6"
|
||||||
|
- "PyQt6.QtNetwork"
|
||||||
|
- "PyQt6.sip"
|
||||||
|
- "logging.handlers"
|
||||||
|
- "zeroconf"
|
||||||
|
- "fcntl"
|
||||||
|
- "stl"
|
||||||
|
- "serial"
|
||||||
|
collect_all:
|
||||||
|
- "cura"
|
||||||
|
- "UM"
|
||||||
|
- "serial"
|
||||||
|
- "Charon"
|
||||||
|
- "sqlite3"
|
||||||
|
- "trimesh"
|
||||||
|
- "win32ctypes"
|
||||||
|
- "PyQt6"
|
||||||
|
- "PyQt6.QtNetwork"
|
||||||
|
- "PyQt6.sip"
|
||||||
|
- "stl"
|
||||||
|
icon:
|
||||||
|
Windows: "./icons/Cura.ico"
|
||||||
|
Macos: "./icons/cura.icns"
|
||||||
|
Linux: "./icons/cura-128.png"
|
||||||
|
"5.2.0-alpha":
|
||||||
|
requirements:
|
||||||
|
- "pyarcus/(latest)@ultimaker/testing"
|
||||||
|
- "curaengine/(latest)@ultimaker/testing"
|
||||||
|
- "pysavitar/(latest)@ultimaker/testing"
|
||||||
|
- "pynest2d/(latest)@ultimaker/testing"
|
||||||
|
- "uranium/(latest)@ultimaker/testing"
|
||||||
|
- "fdm_materials/(latest)@ultimaker/testing"
|
||||||
|
- "cura_binary_data/(latest)@ultimaker/testing"
|
||||||
|
- "cpython/3.10.4"
|
||||||
|
runinfo:
|
||||||
|
entrypoint: "cura_app.py"
|
||||||
|
pyinstaller:
|
||||||
|
datas:
|
||||||
|
cura_plugins:
|
||||||
|
package: "cura"
|
||||||
|
src: "plugins"
|
||||||
|
dst: "share/cura/plugins"
|
||||||
|
cura_resources:
|
||||||
|
package: "cura"
|
||||||
|
src: "resources"
|
||||||
|
dst: "share/cura/resources"
|
||||||
|
uranium_plugins:
|
||||||
|
package: "uranium"
|
||||||
|
src: "plugins"
|
||||||
|
dst: "share/uranium/plugins"
|
||||||
|
uranium_resources:
|
||||||
|
package: "uranium"
|
||||||
|
src: "resources"
|
||||||
|
dst: "share/uranium/resources"
|
||||||
|
uranium_um_qt_qml_um:
|
||||||
|
package: "uranium"
|
||||||
|
src: "site-packages/UM/Qt/qml/UM"
|
||||||
|
dst: "PyQt6/Qt6/qml/UM"
|
||||||
|
cura_binary_data:
|
||||||
|
package: "cura_binary_data"
|
||||||
|
src: "resources/cura/resources"
|
||||||
|
dst: "share/cura/resources"
|
||||||
|
uranium_binary_data:
|
||||||
|
package: "cura_binary_data"
|
||||||
|
src: "resources/uranium/resources"
|
||||||
|
dst: "share/uranium/resources"
|
||||||
|
windows_binary_data:
|
||||||
|
package: "cura_binary_data"
|
||||||
|
src: "windows"
|
||||||
|
dst: "share/windows"
|
||||||
|
fdm_materials:
|
||||||
|
package: "fdm_materials"
|
||||||
|
src: "materials"
|
||||||
|
dst: "share/cura/resources/materials"
|
||||||
|
tcl:
|
||||||
|
package: "tcl"
|
||||||
|
src: "lib/tcl8.6"
|
||||||
|
dst: "tcl"
|
||||||
|
tk:
|
||||||
|
package: "tk"
|
||||||
|
src: "lib/tk8.6"
|
||||||
|
dst: "tk"
|
||||||
|
binaries:
|
||||||
|
curaengine:
|
||||||
|
package: "curaengine"
|
||||||
|
src: "bin"
|
||||||
|
dst: "."
|
||||||
|
binary: "CuraEngine"
|
||||||
|
hiddenimports:
|
||||||
|
- "pySavitar"
|
||||||
|
- "pyArcus"
|
||||||
|
- "pynest2d"
|
||||||
|
- "PyQt6"
|
||||||
|
- "PyQt6.QtNetwork"
|
||||||
|
- "PyQt6.sip"
|
||||||
|
- "logging.handlers"
|
||||||
|
- "zeroconf"
|
||||||
|
- "fcntl"
|
||||||
|
- "stl"
|
||||||
|
- "serial"
|
||||||
|
collect_all:
|
||||||
|
- "cura"
|
||||||
|
- "UM"
|
||||||
|
- "serial"
|
||||||
|
- "Charon"
|
||||||
|
- "sqlite3"
|
||||||
|
- "trimesh"
|
||||||
|
- "win32ctypes"
|
||||||
|
- "PyQt6"
|
||||||
|
- "PyQt6.QtNetwork"
|
||||||
|
- "PyQt6.sip"
|
||||||
|
- "stl"
|
||||||
|
icon:
|
||||||
|
Windows: "./icons/Cura.ico"
|
||||||
|
Macos: "./icons/cura.icns"
|
||||||
|
Linux: "./icons/cura-128.png"
|
||||||
|
"5.1.0":
|
||||||
|
requirements:
|
||||||
|
- "arcus/5.1.0"
|
||||||
|
- "curaengine/5.1.0"
|
||||||
|
- "savitar/5.1.0"
|
||||||
|
- "pynest2d/5.1.0"
|
||||||
|
- "uranium/5.1.0"
|
||||||
|
- "fdm_materials/5.1.0"
|
||||||
|
- "cura_binary_data/5.1.0"
|
||||||
|
- "cpython/3.10.4"
|
||||||
|
runinfo:
|
||||||
|
entrypoint: "cura_app.py"
|
||||||
|
pyinstaller:
|
||||||
|
datas:
|
||||||
|
cura_plugins:
|
||||||
|
package: "cura"
|
||||||
|
src: "plugins"
|
||||||
|
dst: "share/cura/plugins"
|
||||||
|
cura_resources:
|
||||||
|
package: "cura"
|
||||||
|
src: "resources"
|
||||||
|
dst: "share/cura/resources"
|
||||||
|
uranium_plugins:
|
||||||
|
package: "uranium"
|
||||||
|
src: "plugins"
|
||||||
|
dst: "share/uranium/plugins"
|
||||||
|
uranium_resources:
|
||||||
|
package: "uranium"
|
||||||
|
src: "resources"
|
||||||
|
dst: "share/uranium/resources"
|
||||||
|
uranium_um_qt_qml_um:
|
||||||
|
package: "uranium"
|
||||||
|
src: "site-packages/UM/Qt/qml/UM"
|
||||||
|
dst: "PyQt6/Qt6/qml/UM"
|
||||||
|
cura_binary_data:
|
||||||
|
package: "cura_binary_data"
|
||||||
|
src: "resources/cura/resources"
|
||||||
|
dst: "share/cura/resources"
|
||||||
|
uranium_binary_data:
|
||||||
|
package: "cura_binary_data"
|
||||||
|
src: "resources/uranium/resources"
|
||||||
|
dst: "share/uranium/resources"
|
||||||
|
windows_binary_data:
|
||||||
|
package: "cura_binary_data"
|
||||||
|
src: "windows"
|
||||||
|
dst: "share/windows"
|
||||||
|
fdm_materials:
|
||||||
|
package: "fdm_materials"
|
||||||
|
src: "materials"
|
||||||
|
dst: "share/cura/resources/materials"
|
||||||
|
tcl:
|
||||||
|
package: "tcl"
|
||||||
|
src: "lib/tcl8.6"
|
||||||
|
dst: "tcl"
|
||||||
|
tk:
|
||||||
|
package: "tk"
|
||||||
|
src: "lib/tk8.6"
|
||||||
|
dst: "tk"
|
||||||
|
binaries:
|
||||||
|
curaengine:
|
||||||
|
package: "curaengine"
|
||||||
|
src: "bin"
|
||||||
|
dst: "."
|
||||||
|
binary: "CuraEngine"
|
||||||
|
hiddenimports:
|
||||||
|
- "pySavitar"
|
||||||
|
- "pyArcus"
|
||||||
|
- "pynest2d"
|
||||||
|
- "PyQt6"
|
||||||
|
- "PyQt6.QtNetwork"
|
||||||
|
- "PyQt6.sip"
|
||||||
|
- "logging.handlers"
|
||||||
|
- "zeroconf"
|
||||||
|
- "fcntl"
|
||||||
|
- "stl"
|
||||||
|
- "serial"
|
||||||
|
collect_all:
|
||||||
|
- "cura"
|
||||||
|
- "UM"
|
||||||
|
- "serial"
|
||||||
|
- "Charon"
|
||||||
|
- "sqlite3"
|
||||||
|
- "trimesh"
|
||||||
|
- "win32ctypes"
|
||||||
|
- "PyQt6"
|
||||||
|
- "PyQt6.QtNetwork"
|
||||||
|
- "PyQt6.sip"
|
||||||
|
- "stl"
|
||||||
|
icon:
|
||||||
|
Windows: "./icons/Cura.ico"
|
||||||
|
Macos: "./icons/cura.icns"
|
||||||
|
Linux: "./icons/cura-128.png"
|
423
conanfile.py
Normal file
|
@ -0,0 +1,423 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
from platform import python_version
|
||||||
|
|
||||||
|
from jinja2 import Template
|
||||||
|
|
||||||
|
from conans import tools
|
||||||
|
from conan import ConanFile
|
||||||
|
from conan.tools import files
|
||||||
|
from conan.tools.env import VirtualRunEnv, Environment
|
||||||
|
from conan.errors import ConanInvalidConfiguration
|
||||||
|
|
||||||
|
required_conan_version = ">=1.47.0"
|
||||||
|
|
||||||
|
|
||||||
|
class CuraConan(ConanFile):
|
||||||
|
name = "cura"
|
||||||
|
license = "LGPL-3.0"
|
||||||
|
author = "Ultimaker B.V."
|
||||||
|
url = "https://github.com/Ultimaker/cura"
|
||||||
|
description = "3D printer / slicing GUI built on top of the Uranium framework"
|
||||||
|
topics = ("conan", "python", "pyqt5", "qt", "qml", "3d-printing", "slicer")
|
||||||
|
build_policy = "missing"
|
||||||
|
exports = "LICENSE*", "Ultimaker-Cura.spec.jinja", "CuraVersion.py.jinja"
|
||||||
|
settings = "os", "compiler", "build_type", "arch"
|
||||||
|
no_copy_source = True # We won't build so no need to copy sources to the build folder
|
||||||
|
|
||||||
|
# FIXME: Remove specific branch once merged to main
|
||||||
|
# Extending the conanfile with the UMBaseConanfile https://github.com/Ultimaker/conan-ultimaker-index/tree/CURA-9177_Fix_CI_CD/recipes/umbase
|
||||||
|
python_requires = "umbase/0.1.5@ultimaker/testing"
|
||||||
|
python_requires_extend = "umbase.UMBaseConanfile"
|
||||||
|
|
||||||
|
options = {
|
||||||
|
"enterprise": ["True", "False", "true", "false"], # Workaround for GH Action passing boolean as lowercase string
|
||||||
|
"staging": ["True", "False", "true", "false"], # Workaround for GH Action passing boolean as lowercase string
|
||||||
|
"devtools": [True, False], # FIXME: Split this up in testing and (development / build (pyinstaller) / system installer) tools
|
||||||
|
"cloud_api_version": "ANY",
|
||||||
|
"display_name": "ANY", # TODO: should this be an option??
|
||||||
|
"cura_debug_mode": [True, False] # FIXME: Use profiles
|
||||||
|
}
|
||||||
|
default_options = {
|
||||||
|
"enterprise": "False",
|
||||||
|
"staging": "False",
|
||||||
|
"devtools": False,
|
||||||
|
"cloud_api_version": "1",
|
||||||
|
"display_name": "Ultimaker Cura",
|
||||||
|
"cura_debug_mode": False # Not yet implemented
|
||||||
|
}
|
||||||
|
scm = {
|
||||||
|
"type": "git",
|
||||||
|
"subfolder": ".",
|
||||||
|
"url": "auto",
|
||||||
|
"revision": "auto"
|
||||||
|
}
|
||||||
|
|
||||||
|
# TODO: Add unit tests (but they need a different jinja template
|
||||||
|
_pycharm_targets = [{
|
||||||
|
"name": "cura",
|
||||||
|
"module_name": "Cura",
|
||||||
|
"script_name": "cura_app.py",
|
||||||
|
}, {
|
||||||
|
"name": "cura_external_engine",
|
||||||
|
"module_name": "Cura",
|
||||||
|
"script_name": "cura_app.py",
|
||||||
|
"parameters": "--external-backend"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# FIXME: These env vars should be defined in the runenv.
|
||||||
|
_cura_env = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _cura_run_env(self):
|
||||||
|
if self._cura_env:
|
||||||
|
return self._cura_env
|
||||||
|
|
||||||
|
self._cura_env = Environment()
|
||||||
|
self._cura_env.define("QML2_IMPORT_PATH", str(self._site_packages.joinpath("PyQt6", "Qt6", "qml")))
|
||||||
|
self._cura_env.define("QT_PLUGIN_PATH", str(self._site_packages.joinpath("PyQt6", "Qt6", "plugins")))
|
||||||
|
|
||||||
|
if self.settings.os == "Linux":
|
||||||
|
self._cura_env.define("QT_QPA_FONTDIR", "/usr/share/fonts")
|
||||||
|
self._cura_env.define("QT_QPA_PLATFORMTHEME", "xdgdesktopportal")
|
||||||
|
self._cura_env.define("QT_XKB_CONFIG_ROOT", "/usr/share/X11/xkb")
|
||||||
|
return self._cura_env
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _staging(self):
|
||||||
|
return self.options.staging in ["True", 'true']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _enterprise(self):
|
||||||
|
return self.options.enterprise in ["True", 'true']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _cloud_api_root(self):
|
||||||
|
return "https://api-staging.ultimaker.com" if self._staging else "https://api.ultimaker.com"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _cloud_account_api_root(self):
|
||||||
|
return "https://account-staging.ultimaker.com" if self._staging else "https://account.ultimaker.com"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _marketplace_root(self):
|
||||||
|
return "https://marketplace-staging.ultimaker.com" if self._staging else "https://marketplace.ultimaker.com"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _digital_factory_url(self):
|
||||||
|
return "https://digitalfactory-staging.ultimaker.com" if self._staging else "https://digitalfactory.ultimaker.com"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def requirements_txts(self):
|
||||||
|
if self.options.devtools:
|
||||||
|
return ["requirements.txt", "requirements-ultimaker.txt", "requirements-dev.txt"]
|
||||||
|
return ["requirements.txt", "requirements-ultimaker.txt"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _base_dir(self):
|
||||||
|
if self.install_folder is None:
|
||||||
|
if self.build_folder is not None:
|
||||||
|
return Path(self.build_folder)
|
||||||
|
return Path(os.getcwd(), "venv")
|
||||||
|
if self.in_local_cache:
|
||||||
|
return Path(self.install_folder)
|
||||||
|
else:
|
||||||
|
return Path(self.source_folder, "venv")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _share_dir(self):
|
||||||
|
return self._base_dir.joinpath("share")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _script_dir(self):
|
||||||
|
if self.settings.os == "Windows":
|
||||||
|
return self._base_dir.joinpath("Scripts")
|
||||||
|
return self._base_dir.joinpath("bin")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _site_packages(self):
|
||||||
|
if self.settings.os == "Windows":
|
||||||
|
return self._base_dir.joinpath("Lib", "site-packages")
|
||||||
|
py_version = tools.Version(self.deps_cpp_info["cpython"].version)
|
||||||
|
return self._base_dir.joinpath("lib", f"python{py_version.major}.{py_version.minor}", "site-packages")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _py_interp(self):
|
||||||
|
py_interp = self._script_dir.joinpath(Path(self.deps_user_info["cpython"].python).name)
|
||||||
|
if self.settings.os == "Windows":
|
||||||
|
py_interp = Path(*[f'"{p}"' if " " in p else p for p in py_interp.parts])
|
||||||
|
return py_interp
|
||||||
|
|
||||||
|
def _generate_cura_version(self, location):
|
||||||
|
with open(Path(__file__).parent.joinpath("CuraVersion.py.jinja"), "r") as f:
|
||||||
|
cura_version_py = Template(f.read())
|
||||||
|
|
||||||
|
with open(Path(location, "CuraVersion.py"), "w") as f:
|
||||||
|
f.write(cura_version_py.render(
|
||||||
|
cura_app_name = self.name,
|
||||||
|
cura_app_display_name = self.options.display_name,
|
||||||
|
cura_version = self.version,
|
||||||
|
cura_build_type = "Enterprise" if self._enterprise else "",
|
||||||
|
cura_debug_mode = self.options.cura_debug_mode,
|
||||||
|
cura_cloud_api_root = self._cloud_api_root,
|
||||||
|
cura_cloud_api_version = self.options.cloud_api_version,
|
||||||
|
cura_cloud_account_api_root = self._cloud_account_api_root,
|
||||||
|
cura_marketplace_root = self._marketplace_root,
|
||||||
|
cura_digital_factory_url = self._digital_factory_url))
|
||||||
|
|
||||||
|
def _generate_pyinstaller_spec(self, location, entrypoint_location, icon_path, entitlements_file):
|
||||||
|
pyinstaller_metadata = self._um_data()["pyinstaller"]
|
||||||
|
datas = [(str(self._base_dir.joinpath("conan_install_info.json")), ".")]
|
||||||
|
for data in pyinstaller_metadata["datas"].values():
|
||||||
|
if "package" in data: # get the paths from conan package
|
||||||
|
if data["package"] == self.name:
|
||||||
|
if self.in_local_cache:
|
||||||
|
src_path = Path(self.package_folder, data["src"])
|
||||||
|
else:
|
||||||
|
src_path = Path(self.source_folder, data["src"])
|
||||||
|
else:
|
||||||
|
src_path = Path(self.deps_cpp_info[data["package"]].rootpath, data["src"])
|
||||||
|
elif "root" in data: # get the paths relative from the sourcefolder
|
||||||
|
src_path = Path(self.source_folder, data["root"], data["src"])
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
if src_path.exists():
|
||||||
|
datas.append((str(src_path), data["dst"]))
|
||||||
|
|
||||||
|
binaries = []
|
||||||
|
for binary in pyinstaller_metadata["binaries"].values():
|
||||||
|
if "package" in binary: # get the paths from conan package
|
||||||
|
src_path = Path(self.deps_cpp_info[binary["package"]].rootpath, binary["src"])
|
||||||
|
elif "root" in binary: # get the paths relative from the sourcefolder
|
||||||
|
src_path = Path(self.source_folder, binary["root"], binary["src"])
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
if not src_path.exists():
|
||||||
|
continue
|
||||||
|
for bin in src_path.glob(binary["binary"] + ".*[exe|dll|so|dylib]"):
|
||||||
|
binaries.append((str(bin), binary["dst"]))
|
||||||
|
for bin in src_path.glob(binary["binary"]):
|
||||||
|
binaries.append((str(bin), binary["dst"]))
|
||||||
|
|
||||||
|
for _, dependency in self.dependencies.items():
|
||||||
|
for bin_paths in dependency.cpp_info.bindirs:
|
||||||
|
binaries.extend([(f"{p}", ".") for p in Path(bin_paths).glob("**/*.dll")])
|
||||||
|
binaries.extend([(f"{p}", ".") for p in Path(bin_paths).glob("**/*.dylib")])
|
||||||
|
binaries.extend([(f"{p}", ".") for p in Path(bin_paths).glob("**/*.so")])
|
||||||
|
|
||||||
|
# Copy dynamic libs from lib path
|
||||||
|
binaries.extend([(f"{p}", ".") for p in Path(self._base_dir.joinpath("lib")).glob("**/*.dylib")])
|
||||||
|
|
||||||
|
# Collect all dll's from PyQt6 and place them in the root
|
||||||
|
binaries.extend([(f"{p}", ".") for p in Path(self._site_packages, "PyQt6", "Qt6").glob("**/*.dll")])
|
||||||
|
|
||||||
|
with open(Path(__file__).parent.joinpath("Ultimaker-Cura.spec.jinja"), "r") as f:
|
||||||
|
pyinstaller = Template(f.read())
|
||||||
|
|
||||||
|
cura_version = tools.Version(self.version) if self.version else tools.Version("0.0.0")
|
||||||
|
|
||||||
|
with open(Path(location, "Ultimaker-Cura.spec"), "w") as f:
|
||||||
|
f.write(pyinstaller.render(
|
||||||
|
name = str(self.options.display_name).replace(" ", "-"),
|
||||||
|
display_name = self.options.display_name,
|
||||||
|
entrypoint = entrypoint_location,
|
||||||
|
datas = datas,
|
||||||
|
binaries = binaries,
|
||||||
|
venv_script_path = str(self._script_dir),
|
||||||
|
hiddenimports = pyinstaller_metadata["hiddenimports"],
|
||||||
|
collect_all = pyinstaller_metadata["collect_all"],
|
||||||
|
icon = icon_path,
|
||||||
|
entitlements_file = entitlements_file,
|
||||||
|
osx_bundle_identifier = "'nl.ultimaker.cura'" if self.settings.os == "Macos" else "None",
|
||||||
|
upx = str(self.settings.os == "Windows"),
|
||||||
|
strip = False, # This should be possible on Linux and MacOS but, it can also cause issues on some distributions. Safest is to disable it for now
|
||||||
|
target_arch = "'x86_64'" if self.settings.os == "Macos" else "None", # FIXME: Make this dependent on the settings.arch_target
|
||||||
|
macos = self.settings.os == "Macos",
|
||||||
|
version = f"'{self.version}'",
|
||||||
|
short_version = f"'{cura_version.major}.{cura_version.minor}.{cura_version.patch}'",
|
||||||
|
))
|
||||||
|
|
||||||
|
def configure(self):
|
||||||
|
self.options["pyarcus"].shared = True
|
||||||
|
self.options["pysavitar"].shared = True
|
||||||
|
self.options["pynest2d"].shared = True
|
||||||
|
self.options["cpython"].shared = True
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
if self.version and tools.Version(self.version) <= tools.Version("4"):
|
||||||
|
raise ConanInvalidConfiguration("Only versions 5+ are support")
|
||||||
|
|
||||||
|
def requirements(self):
|
||||||
|
for req in self._um_data()["requirements"]:
|
||||||
|
self.requires(req)
|
||||||
|
|
||||||
|
def layout(self):
|
||||||
|
self.folders.source = "."
|
||||||
|
self.folders.build = "venv"
|
||||||
|
self.folders.generators = Path(self.folders.build, "conan")
|
||||||
|
|
||||||
|
self.cpp.package.libdirs = [os.path.join("site-packages", "cura")]
|
||||||
|
self.cpp.package.bindirs = ["bin"]
|
||||||
|
self.cpp.package.resdirs = ["resources", "plugins", "packaging", "pip_requirements"] # pip_requirements should be the last item in the list
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def generate(self):
|
||||||
|
cura_run_envvars = self._cura_run_env.vars(self, scope = "run")
|
||||||
|
ext = ".ps1" if self.settings.os == "Windows" else ".sh"
|
||||||
|
cura_run_envvars.save_script(self.folders.generators.joinpath(f"cura_run_environment{ext}"))
|
||||||
|
|
||||||
|
vr = VirtualRunEnv(self)
|
||||||
|
vr.generate()
|
||||||
|
|
||||||
|
self._generate_cura_version(Path(self.source_folder, "cura"))
|
||||||
|
|
||||||
|
if self.options.devtools:
|
||||||
|
entitlements_file = "'{}'".format(Path(self.source_folder, "packaging", "dmg", "cura.entitlements"))
|
||||||
|
self._generate_pyinstaller_spec(location = self.generators_folder,
|
||||||
|
entrypoint_location = "'{}'".format(Path(self.source_folder, self._um_data()["runinfo"]["entrypoint"])).replace("\\", "\\\\"),
|
||||||
|
icon_path = "'{}'".format(Path(self.source_folder, "packaging", self._um_data()["pyinstaller"]["icon"][str(self.settings.os)])).replace("\\", "\\\\"),
|
||||||
|
entitlements_file = entitlements_file if self.settings.os == "Macos" else "None")
|
||||||
|
|
||||||
|
def imports(self):
|
||||||
|
self.copy("CuraEngine.exe", root_package = "curaengine", src = "@bindirs", dst = "", keep_path = False)
|
||||||
|
self.copy("CuraEngine", root_package = "curaengine", src = "@bindirs", dst = "", keep_path = False)
|
||||||
|
|
||||||
|
files.rmdir(self, "resources/materials")
|
||||||
|
self.copy("*.fdm_material", root_package = "fdm_materials", src = "@resdirs", dst = "resources/materials", keep_path = False)
|
||||||
|
self.copy("*.sig", root_package = "fdm_materials", src = "@resdirs", dst = "resources/materials", keep_path = False)
|
||||||
|
|
||||||
|
# Copy resources of cura_binary_data
|
||||||
|
self.copy("*", root_package = "cura_binary_data", src = self.deps_cpp_info["cura_binary_data"].resdirs[0],
|
||||||
|
dst = "venv/share/cura", keep_path = True)
|
||||||
|
self.copy("*", root_package = "cura_binary_data", src = self.deps_cpp_info["cura_binary_data"].resdirs[1],
|
||||||
|
dst = "venv/share/uranium", keep_path = True)
|
||||||
|
|
||||||
|
self.copy("*.dll", src = "@bindirs", dst = self._site_packages)
|
||||||
|
self.copy("*.pyd", src = "@libdirs", dst = self._site_packages)
|
||||||
|
self.copy("*.pyi", src = "@libdirs", dst = self._site_packages)
|
||||||
|
self.copy("*.dylib", src = "@libdirs", dst = self._script_dir)
|
||||||
|
|
||||||
|
def deploy(self):
|
||||||
|
# Copy CuraEngine.exe to bindirs of Virtual Python Environment
|
||||||
|
# TODO: Fix source such that it will get the curaengine relative from the executable (Python bindir in this case)
|
||||||
|
self.copy_deps("CuraEngine.exe", root_package = "curaengine", src = self.deps_cpp_info["curaengine"].bindirs[0],
|
||||||
|
dst = self._base_dir,
|
||||||
|
keep_path = False)
|
||||||
|
self.copy_deps("CuraEngine", root_package = "curaengine", src = self.deps_cpp_info["curaengine"].bindirs[0], dst = self._base_dir,
|
||||||
|
keep_path = False)
|
||||||
|
|
||||||
|
# Copy resources of Cura (keep folder structure)
|
||||||
|
self.copy("*", src = self.cpp_info.bindirs[0], dst = self._base_dir, keep_path = False)
|
||||||
|
self.copy("*", src = self.cpp_info.libdirs[0], dst = self._site_packages.joinpath("cura"), keep_path = True)
|
||||||
|
self.copy("*", src = self.cpp_info.resdirs[0], dst = self._share_dir.joinpath("cura", "resources"), keep_path = True)
|
||||||
|
self.copy("*", src = self.cpp_info.resdirs[1], dst = self._share_dir.joinpath("cura", "plugins"), keep_path = True)
|
||||||
|
|
||||||
|
# Copy materials (flat)
|
||||||
|
self.copy_deps("*.fdm_material", root_package = "fdm_materials", src = self.deps_cpp_info["fdm_materials"].resdirs[0],
|
||||||
|
dst = self._share_dir.joinpath("cura", "resources", "materials"), keep_path = False)
|
||||||
|
self.copy_deps("*.sig", root_package = "fdm_materials", src = self.deps_cpp_info["fdm_materials"].resdirs[0],
|
||||||
|
dst = self._share_dir.joinpath("cura", "resources", "materials"), keep_path = False)
|
||||||
|
|
||||||
|
# Copy resources of Uranium (keep folder structure)
|
||||||
|
self.copy_deps("*", root_package = "uranium", src = self.deps_cpp_info["uranium"].resdirs[0],
|
||||||
|
dst = self._share_dir.joinpath("uranium", "resources"), keep_path = True)
|
||||||
|
self.copy_deps("*", root_package = "uranium", src = self.deps_cpp_info["uranium"].resdirs[1],
|
||||||
|
dst = self._share_dir.joinpath("uranium", "plugins"), keep_path = True)
|
||||||
|
self.copy_deps("*", root_package = "uranium", src = self.deps_cpp_info["uranium"].libdirs[0],
|
||||||
|
dst = self._site_packages.joinpath("UM"),
|
||||||
|
keep_path = True)
|
||||||
|
self.copy_deps("*", root_package = "uranium", src = str(Path(self.deps_cpp_info["uranium"].libdirs[0], "Qt", "qml", "UM")),
|
||||||
|
dst = self._site_packages.joinpath("PyQt6", "Qt6", "qml", "UM"),
|
||||||
|
keep_path = True)
|
||||||
|
|
||||||
|
# Copy resources of cura_binary_data
|
||||||
|
self.copy_deps("*", root_package = "cura_binary_data", src = self.deps_cpp_info["cura_binary_data"].resdirs[0],
|
||||||
|
dst = self._share_dir.joinpath("cura"), keep_path = True)
|
||||||
|
self.copy_deps("*", root_package = "cura_binary_data", src = self.deps_cpp_info["cura_binary_data"].resdirs[1],
|
||||||
|
dst = self._share_dir.joinpath("uranium"), keep_path = True)
|
||||||
|
if self.settings.os == "Windows":
|
||||||
|
self.copy_deps("*", root_package = "cura_binary_data", src = self.deps_cpp_info["cura_binary_data"].resdirs[2],
|
||||||
|
dst = self._share_dir.joinpath("windows"), keep_path = True)
|
||||||
|
|
||||||
|
self.copy_deps("*.dll", src = "@bindirs", dst = self._site_packages)
|
||||||
|
self.copy_deps("*.pyd", src = "@libdirs", dst = self._site_packages)
|
||||||
|
self.copy_deps("*.pyi", src = "@libdirs", dst = self._site_packages)
|
||||||
|
self.copy_deps("*.dylib", src = "@libdirs", dst = self._base_dir.joinpath("lib"))
|
||||||
|
|
||||||
|
# Copy packaging scripts
|
||||||
|
self.copy("*", src = self.cpp_info.resdirs[2], dst = self._base_dir.joinpath("packaging"))
|
||||||
|
|
||||||
|
# Copy requirements.txt's
|
||||||
|
self.copy("*.txt", src = self.cpp_info.resdirs[-1], dst = self._base_dir.joinpath("pip_requirements"))
|
||||||
|
|
||||||
|
# Generate the GitHub Action version info Environment
|
||||||
|
cura_version = tools.Version(self.version)
|
||||||
|
env_prefix = "Env:" if self.settings.os == "Windows" else ""
|
||||||
|
activate_github_actions_version_env = Template(r"""echo "CURA_VERSION_MAJOR={{ cura_version_major }}" >> ${{ env_prefix }}GITHUB_ENV
|
||||||
|
echo "CURA_VERSION_MINOR={{ cura_version_minor }}" >> ${{ env_prefix }}GITHUB_ENV
|
||||||
|
echo "CURA_VERSION_PATCH={{ cura_version_patch }}" >> ${{ env_prefix }}GITHUB_ENV
|
||||||
|
echo "CURA_VERSION_BUILD={{ cura_version_build }}" >> ${{ env_prefix }}GITHUB_ENV
|
||||||
|
echo "CURA_VERSION_FULL={{ cura_version_full }}" >> ${{ env_prefix }}GITHUB_ENV
|
||||||
|
""").render(cura_version_major = cura_version.major,
|
||||||
|
cura_version_minor = cura_version.minor,
|
||||||
|
cura_version_patch = cura_version.patch,
|
||||||
|
cura_version_build = cura_version.build if cura_version.build != "" else "0",
|
||||||
|
cura_version_full = self.version,
|
||||||
|
env_prefix = env_prefix)
|
||||||
|
|
||||||
|
ext = ".sh" if self.settings.os != "Windows" else ".ps1"
|
||||||
|
files.save(self, self._script_dir.joinpath(f"activate_github_actions_version_env{ext}"), activate_github_actions_version_env)
|
||||||
|
|
||||||
|
self._generate_cura_version(Path(self._site_packages, "cura"))
|
||||||
|
|
||||||
|
entitlements_file = "'{}'".format(Path(self.cpp_info.res_paths[2], "dmg", "cura.entitlements"))
|
||||||
|
self._generate_pyinstaller_spec(location = self._base_dir,
|
||||||
|
entrypoint_location = "'{}'".format(Path(self.cpp_info.bin_paths[0], self._um_data()["runinfo"]["entrypoint"])).replace("\\", "\\\\"),
|
||||||
|
icon_path = "'{}'".format(Path(self.cpp_info.res_paths[2], self._um_data()["pyinstaller"]["icon"][str(self.settings.os)])).replace("\\", "\\\\"),
|
||||||
|
entitlements_file = entitlements_file if self.settings.os == "Macos" else "None")
|
||||||
|
|
||||||
|
def package(self):
|
||||||
|
self.copy("cura_app.py", src = ".", dst = self.cpp.package.bindirs[0])
|
||||||
|
self.copy("*", src = "cura", dst = self.cpp.package.libdirs[0])
|
||||||
|
self.copy("*", src = "resources", dst = self.cpp.package.resdirs[0])
|
||||||
|
self.copy("*", src = "plugins", dst = self.cpp.package.resdirs[1])
|
||||||
|
self.copy("requirement*.txt", src = ".", dst = self.cpp.package.resdirs[-1])
|
||||||
|
self.copy("*", src = "packaging", dst = self.cpp.package.resdirs[2])
|
||||||
|
|
||||||
|
def package_info(self):
|
||||||
|
self.user_info.pip_requirements = "requirements.txt"
|
||||||
|
self.user_info.pip_requirements_git = "requirements-ultimaker.txt"
|
||||||
|
self.user_info.pip_requirements_build = "requirements-dev.txt"
|
||||||
|
|
||||||
|
if self.in_local_cache:
|
||||||
|
self.runenv_info.append_path("PYTHONPATH", str(Path(self.cpp_info.lib_paths[0]).parent))
|
||||||
|
self.runenv_info.append_path("PYTHONPATH", self.cpp_info.res_paths[1]) # Add plugins to PYTHONPATH
|
||||||
|
else:
|
||||||
|
self.runenv_info.append_path("PYTHONPATH", self.source_folder)
|
||||||
|
self.runenv_info.append_path("PYTHONPATH", os.path.join(self.source_folder, "plugins"))
|
||||||
|
|
||||||
|
def package_id(self):
|
||||||
|
del self.info.settings.os
|
||||||
|
del self.info.settings.compiler
|
||||||
|
del self.info.settings.build_type
|
||||||
|
del self.info.settings.arch
|
||||||
|
|
||||||
|
# The following options shouldn't be used to determine the hash, since these are only used to set the CuraVersion.py
|
||||||
|
# which will als be generated by the deploy method during the `conan install cura/5.1.0@_/_`
|
||||||
|
del self.info.options.enterprise
|
||||||
|
del self.info.options.staging
|
||||||
|
del self.info.options.devtools
|
||||||
|
del self.info.options.cloud_api_version
|
||||||
|
del self.info.options.display_name
|
||||||
|
del self.info.options.cura_debug_mode
|
||||||
|
|
||||||
|
# TODO: Use the hash of requirements.txt and requirements-ultimaker.txt, Because changing these will actually result in a different
|
||||||
|
# Cura. This is needed because the requirements.txt aren't managed by Conan and therefor not resolved in the package_id. This isn't
|
||||||
|
# ideal but an acceptable solution for now.
|
|
@ -1,19 +1,26 @@
|
||||||
# Copyright (c) 2021 Ultimaker B.V.
|
# Copyright (c) 2022 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
import enum
|
import enum
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import json
|
||||||
from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty, QTimer, pyqtEnum
|
from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty, QTimer, pyqtEnum
|
||||||
from typing import Any, Optional, Dict, TYPE_CHECKING, Callable
|
from PyQt6.QtNetwork import QNetworkRequest
|
||||||
|
from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
|
from UM.Decorators import deprecated
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
|
||||||
|
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
|
||||||
from cura.OAuth2.AuthorizationService import AuthorizationService
|
from cura.OAuth2.AuthorizationService import AuthorizationService
|
||||||
from cura.OAuth2.Models import OAuth2Settings, UserProfile
|
from cura.OAuth2.Models import OAuth2Settings, UserProfile
|
||||||
from cura.UltimakerCloud import UltimakerCloudConstants
|
from cura.UltimakerCloud import UltimakerCloudConstants
|
||||||
|
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
|
from PyQt6.QtNetwork import QNetworkReply
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
@ -78,6 +85,7 @@ class Account(QObject):
|
||||||
self._logged_in = False
|
self._logged_in = False
|
||||||
self._user_profile: Optional[UserProfile] = None
|
self._user_profile: Optional[UserProfile] = None
|
||||||
self._additional_rights: Dict[str, Any] = {}
|
self._additional_rights: Dict[str, Any] = {}
|
||||||
|
self._permissions: List[str] = [] # List of account permission keys, e.g. ["digital-factory.print-job.write"]
|
||||||
self._sync_state = SyncState.IDLE
|
self._sync_state = SyncState.IDLE
|
||||||
self._manual_sync_enabled = False
|
self._manual_sync_enabled = False
|
||||||
self._update_packages_enabled = False
|
self._update_packages_enabled = False
|
||||||
|
@ -109,6 +117,7 @@ class Account(QObject):
|
||||||
|
|
||||||
self._sync_services: Dict[str, int] = {}
|
self._sync_services: Dict[str, int] = {}
|
||||||
"""contains entries "service_name" : SyncState"""
|
"""contains entries "service_name" : SyncState"""
|
||||||
|
self.syncRequested.connect(self._updatePermissions)
|
||||||
|
|
||||||
def initialize(self) -> None:
|
def initialize(self) -> None:
|
||||||
self._authorization_service.initialize(self._application.getPreferences())
|
self._authorization_service.initialize(self._application.getPreferences())
|
||||||
|
@ -311,13 +320,63 @@ class Account(QObject):
|
||||||
|
|
||||||
self._authorization_service.deleteAuthData()
|
self._authorization_service.deleteAuthData()
|
||||||
|
|
||||||
|
@deprecated("Get permissions from the 'permissions' property", since = "5.2.0")
|
||||||
def updateAdditionalRight(self, **kwargs) -> None:
|
def updateAdditionalRight(self, **kwargs) -> None:
|
||||||
"""Update the additional rights of the account.
|
"""Update the additional rights of the account.
|
||||||
The argument(s) are the rights that need to be set"""
|
The argument(s) are the rights that need to be set"""
|
||||||
self._additional_rights.update(kwargs)
|
self._additional_rights.update(kwargs)
|
||||||
self.additionalRightsChanged.emit(self._additional_rights)
|
self.additionalRightsChanged.emit(self._additional_rights)
|
||||||
|
|
||||||
|
@deprecated("Get permissions from the 'permissions' property", since = "5.2.0")
|
||||||
@pyqtProperty("QVariantMap", notify = additionalRightsChanged)
|
@pyqtProperty("QVariantMap", notify = additionalRightsChanged)
|
||||||
def additionalRights(self) -> Dict[str, Any]:
|
def additionalRights(self) -> Dict[str, Any]:
|
||||||
"""A dictionary which can be queried for additional account rights."""
|
"""A dictionary which can be queried for additional account rights."""
|
||||||
return self._additional_rights
|
return self._additional_rights
|
||||||
|
|
||||||
|
permissionsChanged = pyqtSignal()
|
||||||
|
@pyqtProperty("QVariantList", notify = permissionsChanged)
|
||||||
|
def permissions(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
The permission keys that the user has in his account.
|
||||||
|
"""
|
||||||
|
return self._permissions
|
||||||
|
|
||||||
|
def _updatePermissions(self) -> None:
|
||||||
|
"""
|
||||||
|
Update the list of permissions that the user has.
|
||||||
|
"""
|
||||||
|
def callback(reply: "QNetworkReply"):
|
||||||
|
status_code = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute)
|
||||||
|
if status_code is None:
|
||||||
|
Logger.error("Server did not respond to request to get list of permissions.")
|
||||||
|
return
|
||||||
|
if status_code >= 300:
|
||||||
|
Logger.error(f"Request to get list of permission resulted in HTTP error {status_code}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
reply_data = json.loads(bytes(reply.readAll()).decode("UTF-8"))
|
||||||
|
except (UnicodeDecodeError, json.JSONDecodeError, ValueError) as e:
|
||||||
|
Logger.logException("e", f"Could not parse response to permission list request: {e}")
|
||||||
|
return
|
||||||
|
if "errors" in reply_data:
|
||||||
|
Logger.error(f"Request to get list of permission resulted in error response: {reply_data['errors']}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if "data" in reply_data and "permissions" in reply_data["data"]:
|
||||||
|
permissions = sorted(reply_data["data"]["permissions"])
|
||||||
|
if permissions != self._permissions:
|
||||||
|
self._permissions = permissions
|
||||||
|
self.permissionsChanged.emit()
|
||||||
|
|
||||||
|
def error_callback(reply: "QNetworkReply", error: "QNetworkReply.NetworkError"):
|
||||||
|
Logger.error(f"Request for user permissions list failed. Network error: {error}")
|
||||||
|
|
||||||
|
HttpRequestManager.getInstance().get(
|
||||||
|
url = f"{self._oauth_root}/users/permissions",
|
||||||
|
scope = JsonDecoratorScope(UltimakerCloudScope(self._application)),
|
||||||
|
callback = callback,
|
||||||
|
error_callback = error_callback,
|
||||||
|
timeout = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (c) 2021 Ultimaker B.V.
|
# Copyright (c) 2022 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
# ---------
|
# ---------
|
||||||
|
@ -6,14 +6,14 @@
|
||||||
# ---------
|
# ---------
|
||||||
DEFAULT_CURA_APP_NAME = "cura"
|
DEFAULT_CURA_APP_NAME = "cura"
|
||||||
DEFAULT_CURA_DISPLAY_NAME = "Ultimaker Cura"
|
DEFAULT_CURA_DISPLAY_NAME = "Ultimaker Cura"
|
||||||
DEFAULT_CURA_VERSION = "master"
|
DEFAULT_CURA_VERSION = "dev"
|
||||||
DEFAULT_CURA_BUILD_TYPE = ""
|
DEFAULT_CURA_BUILD_TYPE = ""
|
||||||
DEFAULT_CURA_DEBUG_MODE = False
|
DEFAULT_CURA_DEBUG_MODE = False
|
||||||
|
|
||||||
# Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for
|
# Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for
|
||||||
# example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the
|
# example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the
|
||||||
# CuraVersion.py.in template.
|
# CuraVersion.py.in template.
|
||||||
CuraSDKVersion = "7.9.0"
|
CuraSDKVersion = "8.1.0"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from cura.CuraVersion import CuraAppName # type: ignore
|
from cura.CuraVersion import CuraAppName # type: ignore
|
||||||
|
@ -60,3 +60,14 @@ try:
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME
|
CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME
|
||||||
|
|
||||||
|
DEPENDENCY_INFO = {}
|
||||||
|
try:
|
||||||
|
from pathlib import Path
|
||||||
|
conan_install_info = Path(__file__).parent.parent.joinpath("conan_install_info.json")
|
||||||
|
if conan_install_info.exists():
|
||||||
|
import json
|
||||||
|
with open(conan_install_info, "r") as f:
|
||||||
|
DEPENDENCY_INFO = json.loads(f.read())
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
|
@ -136,7 +136,7 @@ class Backup:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
current_version = Version(self._application.getVersion())
|
current_version = Version(self._application.getVersion())
|
||||||
version_to_restore = Version(self.meta_data.get("cura_release", "master"))
|
version_to_restore = Version(self.meta_data.get("cura_release", "dev"))
|
||||||
|
|
||||||
if current_version < version_to_restore:
|
if current_version < version_to_restore:
|
||||||
# Cannot restore version newer than current because settings might have changed.
|
# Cannot restore version newer than current because settings might have changed.
|
||||||
|
|
|
@ -565,8 +565,8 @@ class BuildVolume(SceneNode):
|
||||||
self._updateScaleFactor()
|
self._updateScaleFactor()
|
||||||
|
|
||||||
self._volume_aabb = AxisAlignedBox(
|
self._volume_aabb = AxisAlignedBox(
|
||||||
minimum = Vector(min_w, min_h - 1.0, min_d).scale(self._scale_vector),
|
minimum = Vector(min_w, min_h - 1.0, min_d),
|
||||||
maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d).scale(self._scale_vector)
|
maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d)
|
||||||
)
|
)
|
||||||
|
|
||||||
bed_adhesion_size = self.getEdgeDisallowedSize()
|
bed_adhesion_size = self.getEdgeDisallowedSize()
|
||||||
|
@ -575,8 +575,8 @@ class BuildVolume(SceneNode):
|
||||||
# This is probably wrong in all other cases. TODO!
|
# This is probably wrong in all other cases. TODO!
|
||||||
# The +1 and -1 is added as there is always a bit of extra room required to work properly.
|
# The +1 and -1 is added as there is always a bit of extra room required to work properly.
|
||||||
scale_to_max_bounds = AxisAlignedBox(
|
scale_to_max_bounds = AxisAlignedBox(
|
||||||
minimum = Vector(min_w + bed_adhesion_size + 1, min_h, min_d + self._disallowed_area_size - bed_adhesion_size + 1).scale(self._scale_vector),
|
minimum = Vector(min_w + bed_adhesion_size + 1, min_h, min_d + self._disallowed_area_size - bed_adhesion_size + 1),
|
||||||
maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - self._disallowed_area_size + bed_adhesion_size - 1).scale(self._scale_vector)
|
maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - self._disallowed_area_size + bed_adhesion_size - 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds # type: ignore
|
self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds # type: ignore
|
||||||
|
@ -645,7 +645,7 @@ class BuildVolume(SceneNode):
|
||||||
for extruder in extruders:
|
for extruder in extruders:
|
||||||
extruder.propertyChanged.connect(self._onSettingPropertyChanged)
|
extruder.propertyChanged.connect(self._onSettingPropertyChanged)
|
||||||
|
|
||||||
self._width = self._global_container_stack.getProperty("machine_width", "value") * self._scale_vector.x
|
self._width = self._global_container_stack.getProperty("machine_width", "value")
|
||||||
machine_height = self._global_container_stack.getProperty("machine_height", "value")
|
machine_height = self._global_container_stack.getProperty("machine_height", "value")
|
||||||
if self._global_container_stack.getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
|
if self._global_container_stack.getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
|
||||||
self._height = min(self._global_container_stack.getProperty("gantry_height", "value") * self._scale_vector.z, machine_height)
|
self._height = min(self._global_container_stack.getProperty("gantry_height", "value") * self._scale_vector.z, machine_height)
|
||||||
|
@ -656,7 +656,7 @@ class BuildVolume(SceneNode):
|
||||||
else:
|
else:
|
||||||
self._height = self._global_container_stack.getProperty("machine_height", "value")
|
self._height = self._global_container_stack.getProperty("machine_height", "value")
|
||||||
self._build_volume_message.hide()
|
self._build_volume_message.hide()
|
||||||
self._depth = self._global_container_stack.getProperty("machine_depth", "value") * self._scale_vector.y
|
self._depth = self._global_container_stack.getProperty("machine_depth", "value")
|
||||||
self._shape = self._global_container_stack.getProperty("machine_shape", "value")
|
self._shape = self._global_container_stack.getProperty("machine_shape", "value")
|
||||||
|
|
||||||
self._updateDisallowedAreas()
|
self._updateDisallowedAreas()
|
||||||
|
@ -752,8 +752,8 @@ class BuildVolume(SceneNode):
|
||||||
return
|
return
|
||||||
self._updateScaleFactor()
|
self._updateScaleFactor()
|
||||||
self._height = self._global_container_stack.getProperty("machine_height", "value") * self._scale_vector.z
|
self._height = self._global_container_stack.getProperty("machine_height", "value") * self._scale_vector.z
|
||||||
self._width = self._global_container_stack.getProperty("machine_width", "value") * self._scale_vector.x
|
self._width = self._global_container_stack.getProperty("machine_width", "value")
|
||||||
self._depth = self._global_container_stack.getProperty("machine_depth", "value") * self._scale_vector.y
|
self._depth = self._global_container_stack.getProperty("machine_depth", "value")
|
||||||
self._shape = self._global_container_stack.getProperty("machine_shape", "value")
|
self._shape = self._global_container_stack.getProperty("machine_shape", "value")
|
||||||
|
|
||||||
def _updateDisallowedAreasAndRebuild(self):
|
def _updateDisallowedAreasAndRebuild(self):
|
||||||
|
@ -770,14 +770,6 @@ class BuildVolume(SceneNode):
|
||||||
self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks())
|
self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks())
|
||||||
self.rebuild()
|
self.rebuild()
|
||||||
|
|
||||||
def _scaleAreas(self, result_areas: List[Polygon]) -> None:
|
|
||||||
if self._global_container_stack is None:
|
|
||||||
return
|
|
||||||
for i, polygon in enumerate(result_areas):
|
|
||||||
result_areas[i] = polygon.scale(
|
|
||||||
100.0 / max(100.0, self._global_container_stack.getProperty("material_shrinkage_percentage_xy", "value"))
|
|
||||||
)
|
|
||||||
|
|
||||||
def _updateDisallowedAreas(self) -> None:
|
def _updateDisallowedAreas(self) -> None:
|
||||||
if not self._global_container_stack:
|
if not self._global_container_stack:
|
||||||
return
|
return
|
||||||
|
@ -833,11 +825,9 @@ class BuildVolume(SceneNode):
|
||||||
|
|
||||||
self._disallowed_areas = []
|
self._disallowed_areas = []
|
||||||
for extruder_id in result_areas:
|
for extruder_id in result_areas:
|
||||||
self._scaleAreas(result_areas[extruder_id])
|
|
||||||
self._disallowed_areas.extend(result_areas[extruder_id])
|
self._disallowed_areas.extend(result_areas[extruder_id])
|
||||||
self._disallowed_areas_no_brim = []
|
self._disallowed_areas_no_brim = []
|
||||||
for extruder_id in result_areas_no_brim:
|
for extruder_id in result_areas_no_brim:
|
||||||
self._scaleAreas(result_areas_no_brim[extruder_id])
|
|
||||||
self._disallowed_areas_no_brim.extend(result_areas_no_brim[extruder_id])
|
self._disallowed_areas_no_brim.extend(result_areas_no_brim[extruder_id])
|
||||||
|
|
||||||
def _computeDisallowedAreasPrinted(self, used_extruders):
|
def _computeDisallowedAreasPrinted(self, used_extruders):
|
||||||
|
@ -993,6 +983,9 @@ class BuildVolume(SceneNode):
|
||||||
half_machine_width = self._global_container_stack.getProperty("machine_width", "value") / 2
|
half_machine_width = self._global_container_stack.getProperty("machine_width", "value") / 2
|
||||||
half_machine_depth = self._global_container_stack.getProperty("machine_depth", "value") / 2
|
half_machine_depth = self._global_container_stack.getProperty("machine_depth", "value") / 2
|
||||||
|
|
||||||
|
# We need at a minimum a very small border around the edge so that models can't go off the build plate
|
||||||
|
border_size = max(border_size, 0.1)
|
||||||
|
|
||||||
if self._shape != "elliptic":
|
if self._shape != "elliptic":
|
||||||
if border_size - left_unreachable_border > 0:
|
if border_size - left_unreachable_border > 0:
|
||||||
result[extruder_id].append(Polygon(numpy.array([
|
result[extruder_id].append(Polygon(numpy.array([
|
||||||
|
|
|
@ -261,7 +261,7 @@ class CrashHandler:
|
||||||
opengl_instance = OpenGL.getInstance()
|
opengl_instance = OpenGL.getInstance()
|
||||||
if not opengl_instance:
|
if not opengl_instance:
|
||||||
self.data["opengl"] = {"version": "n/a", "vendor": "n/a", "type": "n/a"}
|
self.data["opengl"] = {"version": "n/a", "vendor": "n/a", "type": "n/a"}
|
||||||
return catalog.i18nc("@label", "Not yet initialized<br/>")
|
return catalog.i18nc("@label", "Not yet initialized") + "<br />"
|
||||||
|
|
||||||
info = "<ul>"
|
info = "<ul>"
|
||||||
info += catalog.i18nc("@label OpenGL version", "<li>OpenGL Version: {version}</li>").format(version = opengl_instance.getOpenGLVersion())
|
info += catalog.i18nc("@label OpenGL version", "<li>OpenGL Version: {version}</li>").format(version = opengl_instance.getOpenGLVersion())
|
||||||
|
@ -291,6 +291,7 @@ class CrashHandler:
|
||||||
if with_sentry_sdk:
|
if with_sentry_sdk:
|
||||||
with configure_scope() as scope:
|
with configure_scope() as scope:
|
||||||
scope.set_tag("opengl_version", opengl_instance.getOpenGLVersion())
|
scope.set_tag("opengl_version", opengl_instance.getOpenGLVersion())
|
||||||
|
scope.set_tag("opengl_version_short", opengl_instance.getOpenGLVersionShort())
|
||||||
scope.set_tag("gpu_vendor", opengl_instance.getGPUVendorName())
|
scope.set_tag("gpu_vendor", opengl_instance.getGPUVendorName())
|
||||||
scope.set_tag("gpu_type", opengl_instance.getGPUType())
|
scope.set_tag("gpu_type", opengl_instance.getGPUType())
|
||||||
scope.set_tag("active_machine", active_machine_definition_id)
|
scope.set_tag("active_machine", active_machine_definition_id)
|
||||||
|
|
|
@ -115,6 +115,8 @@ from . import CuraActions
|
||||||
from . import PlatformPhysics
|
from . import PlatformPhysics
|
||||||
from . import PrintJobPreviewImageProvider
|
from . import PrintJobPreviewImageProvider
|
||||||
from .AutoSave import AutoSave
|
from .AutoSave import AutoSave
|
||||||
|
from .Machines.Models.ActiveIntentQualitiesModel import ActiveIntentQualitiesModel
|
||||||
|
from .Machines.Models.IntentSelectionModel import IntentSelectionModel
|
||||||
from .SingleInstance import SingleInstance
|
from .SingleInstance import SingleInstance
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -257,6 +259,7 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
from UM.CentralFileStorage import CentralFileStorage
|
from UM.CentralFileStorage import CentralFileStorage
|
||||||
CentralFileStorage.setIsEnterprise(ApplicationMetadata.IsEnterpriseVersion)
|
CentralFileStorage.setIsEnterprise(ApplicationMetadata.IsEnterpriseVersion)
|
||||||
|
Resources.setIsEnterprise(ApplicationMetadata.IsEnterpriseVersion)
|
||||||
|
|
||||||
@pyqtProperty(str, constant=True)
|
@pyqtProperty(str, constant=True)
|
||||||
def ultimakerCloudApiRootUrl(self) -> str:
|
def ultimakerCloudApiRootUrl(self) -> str:
|
||||||
|
@ -315,7 +318,7 @@ class CuraApplication(QtApplication):
|
||||||
def initialize(self) -> None:
|
def initialize(self) -> None:
|
||||||
self.__addExpectedResourceDirsAndSearchPaths() # Must be added before init of super
|
self.__addExpectedResourceDirsAndSearchPaths() # Must be added before init of super
|
||||||
|
|
||||||
super().initialize()
|
super().initialize(ApplicationMetadata.IsEnterpriseVersion)
|
||||||
|
|
||||||
self._preferences.addPreference("cura/single_instance", False)
|
self._preferences.addPreference("cura/single_instance", False)
|
||||||
self._use_single_instance = self._preferences.getValue("cura/single_instance") or self._cli_args.single_instance
|
self._use_single_instance = self._preferences.getValue("cura/single_instance") or self._cli_args.single_instance
|
||||||
|
@ -348,13 +351,18 @@ class CuraApplication(QtApplication):
|
||||||
Resources.addExpectedDirNameInData(dir_name)
|
Resources.addExpectedDirNameInData(dir_name)
|
||||||
|
|
||||||
app_root = os.path.abspath(os.path.join(os.path.dirname(sys.executable)))
|
app_root = os.path.abspath(os.path.join(os.path.dirname(sys.executable)))
|
||||||
Resources.addSearchPath(os.path.join(app_root, "share", "cura", "resources"))
|
Resources.addSecureSearchPath(os.path.join(app_root, "share", "cura", "resources"))
|
||||||
Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "share", "cura", "resources"))
|
|
||||||
|
|
||||||
Resources.addSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources"))
|
Resources.addSecureSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources"))
|
||||||
if not hasattr(sys, "frozen"):
|
if not hasattr(sys, "frozen"):
|
||||||
resource_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources")
|
Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources"))
|
||||||
Resources.addSearchPath(resource_path)
|
|
||||||
|
# local Conan cache
|
||||||
|
Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "..", "resources"))
|
||||||
|
Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "..", "plugins"))
|
||||||
|
|
||||||
|
# venv site-packages
|
||||||
|
Resources.addSearchPath(os.path.join(app_root, "..", "share", "cura", "resources"))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _initializeSettingDefinitions(cls):
|
def _initializeSettingDefinitions(cls):
|
||||||
|
@ -812,6 +820,12 @@ class CuraApplication(QtApplication):
|
||||||
def run(self):
|
def run(self):
|
||||||
super().run()
|
super().run()
|
||||||
|
|
||||||
|
if len(ApplicationMetadata.DEPENDENCY_INFO) > 0:
|
||||||
|
Logger.debug("Using Conan managed dependencies: " + ", ".join(
|
||||||
|
[dep["recipe"]["id"] for dep in ApplicationMetadata.DEPENDENCY_INFO["installed"] if dep["recipe"]["version"] != "latest"]))
|
||||||
|
else:
|
||||||
|
Logger.warning("Could not find conan_install_info.json")
|
||||||
|
|
||||||
Logger.log("i", "Initializing machine error checker")
|
Logger.log("i", "Initializing machine error checker")
|
||||||
self._machine_error_checker = MachineErrorChecker(self)
|
self._machine_error_checker = MachineErrorChecker(self)
|
||||||
self._machine_error_checker.initialize()
|
self._machine_error_checker.initialize()
|
||||||
|
@ -943,6 +957,7 @@ class CuraApplication(QtApplication):
|
||||||
self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles))
|
self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles))
|
||||||
self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing engine..."))
|
self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing engine..."))
|
||||||
self.initializeEngine()
|
self.initializeEngine()
|
||||||
|
self.getTheme().setCheckIfTrusted(ApplicationMetadata.IsEnterpriseVersion)
|
||||||
|
|
||||||
# Initialize UI state
|
# Initialize UI state
|
||||||
controller.setActiveStage("PrepareStage")
|
controller.setActiveStage("PrepareStage")
|
||||||
|
@ -1191,6 +1206,8 @@ class CuraApplication(QtApplication):
|
||||||
qmlRegisterType(NozzleModel, "Cura", 1, 0, "NozzleModel")
|
qmlRegisterType(NozzleModel, "Cura", 1, 0, "NozzleModel")
|
||||||
qmlRegisterType(IntentModel, "Cura", 1, 6, "IntentModel")
|
qmlRegisterType(IntentModel, "Cura", 1, 6, "IntentModel")
|
||||||
qmlRegisterType(IntentCategoryModel, "Cura", 1, 6, "IntentCategoryModel")
|
qmlRegisterType(IntentCategoryModel, "Cura", 1, 6, "IntentCategoryModel")
|
||||||
|
qmlRegisterType(IntentSelectionModel, "Cura", 1, 7, "IntentSelectionModel")
|
||||||
|
qmlRegisterType(ActiveIntentQualitiesModel, "Cura", 1, 7, "ActiveIntentQualitiesModel")
|
||||||
|
|
||||||
self.processEvents()
|
self.processEvents()
|
||||||
qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
|
qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
|
||||||
|
|
|
@ -1,14 +1,21 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, cast, Dict, List, Set, Tuple, TYPE_CHECKING, Optional
|
from typing import Any, cast, Dict, List, Set, Tuple, TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.PluginRegistry import PluginRegistry
|
||||||
from cura.CuraApplication import CuraApplication # To find some resource types.
|
from cura.CuraApplication import CuraApplication # To find some resource types.
|
||||||
from cura.Settings.GlobalStack import GlobalStack
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
|
|
||||||
from UM.PackageManager import PackageManager # The class we're extending.
|
from UM.PackageManager import PackageManager # The class we're extending.
|
||||||
from UM.Resources import Resources # To find storage paths for some resource types.
|
from UM.Resources import Resources # To find storage paths for some resource types.
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
from urllib.parse import unquote_plus
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -48,9 +55,62 @@ class CuraPackageManager(PackageManager):
|
||||||
def initialize(self) -> None:
|
def initialize(self) -> None:
|
||||||
self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer)
|
self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer)
|
||||||
self._installation_dirs_dict["qualities"] = Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer)
|
self._installation_dirs_dict["qualities"] = Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer)
|
||||||
|
self._installation_dirs_dict["variants"] = Resources.getStoragePath(CuraApplication.ResourceTypes.VariantInstanceContainer)
|
||||||
|
|
||||||
|
# Due to a bug in Cura 5.1.0 we needed to change the directory structure of the curapackage on the server side (See SD-3871).
|
||||||
|
# Although the material intent profiles will be installed in the `intent` folder, the curapackage from the server side will
|
||||||
|
# have an `intents` folder. For completeness, we will look in both locations of in the curapackage and map them both to the
|
||||||
|
# `intent` folder.
|
||||||
|
self._installation_dirs_dict["intents"] = Resources.getStoragePath(CuraApplication.ResourceTypes.IntentInstanceContainer)
|
||||||
|
self._installation_dirs_dict["intent"] = Resources.getStoragePath(CuraApplication.ResourceTypes.IntentInstanceContainer)
|
||||||
|
|
||||||
super().initialize()
|
super().initialize()
|
||||||
|
|
||||||
|
def isMaterialBundled(self, file_name: str, guid: str):
|
||||||
|
""" Check if there is a bundled material name with file_name and guid """
|
||||||
|
for path in Resources.getSecureSearchPaths():
|
||||||
|
# Secure search paths are install directory paths, if a material is in here it must be bundled.
|
||||||
|
|
||||||
|
paths = [Path(p) for p in glob.glob(path + '/**/*.xml.fdm_material', recursive=True)]
|
||||||
|
for material in paths:
|
||||||
|
if material.name == file_name:
|
||||||
|
Logger.info(f"Found bundled material: {material.name}. Located in path: {str(material)}")
|
||||||
|
with open(material, encoding="utf-8") as f:
|
||||||
|
# Make sure the file we found has the same guid as our material
|
||||||
|
# Parsing this xml would be better but the namespace is needed to search it.
|
||||||
|
parsed_guid = PluginRegistry.getInstance().getPluginObject(
|
||||||
|
"XmlMaterialProfile").getMetadataFromSerialized(
|
||||||
|
f.read(), "GUID")
|
||||||
|
if guid == parsed_guid:
|
||||||
|
# The material we found matches both filename and GUID
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def getMaterialFilePackageId(self, file_name: str, guid: str) -> str:
|
||||||
|
"""Get the id of the installed material package that contains file_name"""
|
||||||
|
file_name = unquote_plus(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))
|
||||||
|
Logger.error(f"Bundled paths searched: {list(Resources.getSecureSearchPaths())}")
|
||||||
|
return ""
|
||||||
|
|
||||||
def getMachinesUsingPackage(self, package_id: str) -> Tuple[List[Tuple[GlobalStack, str, str]], List[Tuple[GlobalStack, str, str]]]:
|
def getMachinesUsingPackage(self, package_id: str) -> Tuple[List[Tuple[GlobalStack, str, str]], List[Tuple[GlobalStack, str, str]]]:
|
||||||
"""Returns a list of where the package is used
|
"""Returns a list of where the package is used
|
||||||
|
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
# Copyright (c) 2020 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
CuraAppName = "@CURA_APP_NAME@"
|
|
||||||
CuraAppDisplayName = "@CURA_APP_DISPLAY_NAME@"
|
|
||||||
CuraVersion = "@CURA_VERSION@"
|
|
||||||
CuraBuildType = "@CURA_BUILDTYPE@"
|
|
||||||
CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
|
|
||||||
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
|
|
||||||
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
|
|
||||||
CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@"
|
|
||||||
CuraMarketplaceRoot = "@CURA_MARKETPLACE_ROOT@"
|
|
||||||
CuraDigitalFactoryURL = "@CURA_DIGITAL_FACTORY_URL@"
|
|
|
@ -53,6 +53,8 @@ class MachineErrorChecker(QObject):
|
||||||
|
|
||||||
self._keys_to_check = set() # type: Set[str]
|
self._keys_to_check = set() # type: Set[str]
|
||||||
|
|
||||||
|
self._num_keys_to_check_per_update = 10
|
||||||
|
|
||||||
def initialize(self) -> None:
|
def initialize(self) -> None:
|
||||||
self._error_check_timer.timeout.connect(self._rescheduleCheck)
|
self._error_check_timer.timeout.connect(self._rescheduleCheck)
|
||||||
|
|
||||||
|
@ -162,37 +164,37 @@ class MachineErrorChecker(QObject):
|
||||||
|
|
||||||
self._check_in_progress = True
|
self._check_in_progress = True
|
||||||
|
|
||||||
# If there is nothing to check any more, it means there is no error.
|
for i in range(self._num_keys_to_check_per_update):
|
||||||
if not self._stacks_and_keys_to_check:
|
# If there is nothing to check any more, it means there is no error.
|
||||||
# Finish
|
if not self._stacks_and_keys_to_check:
|
||||||
self._setResult(False)
|
# Finish
|
||||||
return
|
self._setResult(False)
|
||||||
|
return
|
||||||
|
|
||||||
# Get the next stack and key to check
|
# Get the next stack and key to check
|
||||||
stack, key = self._stacks_and_keys_to_check.popleft()
|
stack, key = self._stacks_and_keys_to_check.popleft()
|
||||||
|
|
||||||
enabled = stack.getProperty(key, "enabled")
|
enabled = stack.getProperty(key, "enabled")
|
||||||
if not enabled:
|
if not enabled:
|
||||||
self._application.callLater(self._checkStack)
|
continue
|
||||||
return
|
|
||||||
|
|
||||||
validation_state = stack.getProperty(key, "validationState")
|
validation_state = stack.getProperty(key, "validationState")
|
||||||
if validation_state is None:
|
if validation_state is None:
|
||||||
# Setting is not validated. This can happen if there is only a setting definition.
|
# Setting is not validated. This can happen if there is only a setting definition.
|
||||||
# We do need to validate it, because a setting definitions value can be set by a function, which could
|
# We do need to validate it, because a setting definitions value can be set by a function, which could
|
||||||
# be an invalid setting.
|
# be an invalid setting.
|
||||||
definition = stack.getSettingDefinition(key)
|
definition = stack.getSettingDefinition(key)
|
||||||
validator_type = SettingDefinition.getValidatorForType(definition.type)
|
validator_type = SettingDefinition.getValidatorForType(definition.type)
|
||||||
if validator_type:
|
if validator_type:
|
||||||
validator = validator_type(key)
|
validator = validator_type(key)
|
||||||
validation_state = validator(stack)
|
validation_state = validator(stack)
|
||||||
if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError, ValidatorState.Invalid):
|
if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError, ValidatorState.Invalid):
|
||||||
# Since we don't know if any of the settings we didn't check is has an error value, store the list for the
|
# Since we don't know if any of the settings we didn't check is has an error value, store the list for the
|
||||||
# next check.
|
# next check.
|
||||||
keys_to_recheck = {setting_key for stack, setting_key in self._stacks_and_keys_to_check}
|
keys_to_recheck = {setting_key for stack, setting_key in self._stacks_and_keys_to_check}
|
||||||
keys_to_recheck.add(key)
|
keys_to_recheck.add(key)
|
||||||
self._setResult(True, keys_to_recheck = keys_to_recheck)
|
self._setResult(True, keys_to_recheck = keys_to_recheck)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Schedule the check for the next key
|
# Schedule the check for the next key
|
||||||
self._application.callLater(self._checkStack)
|
self._application.callLater(self._checkStack)
|
||||||
|
|
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
|
continue
|
||||||
|
|
||||||
device_name = container_stack.getMetaDataEntry("group_name", container_stack.getName())
|
device_name = container_stack.getMetaDataEntry("group_name", container_stack.getName())
|
||||||
section_name = "Connected printers" if has_remote_connection else "Preset printers"
|
section_name = self._catalog.i18nc("@label", "Connected printers") if has_remote_connection else self._catalog.i18nc("@label", "Preset printers")
|
||||||
section_name = self._catalog.i18nc("@info:title", section_name)
|
section_name = self._catalog.i18nc("@info:title", section_name)
|
||||||
|
|
||||||
default_removal_warning = self._catalog.i18nc(
|
default_removal_warning = self._catalog.i18nc(
|
||||||
|
|
|
@ -111,7 +111,7 @@ class IntentCategoryModel(ListModel):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
weight = 99
|
weight = 99
|
||||||
result.append({
|
result.append({
|
||||||
"name": IntentCategoryModel.translation(category, "name", category),
|
"name": IntentCategoryModel.translation(category, "name", category.title()),
|
||||||
"description": IntentCategoryModel.translation(category, "description", None),
|
"description": IntentCategoryModel.translation(category, "description", None),
|
||||||
"intent_category": category,
|
"intent_category": category,
|
||||||
"weight": weight,
|
"weight": weight,
|
||||||
|
|
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:
|
def __init__(self, parent: Optional[QObject] = None) -> None:
|
||||||
super().__init__(parent = parent)
|
super().__init__(parent = parent)
|
||||||
self._material_sync = CloudMaterialSync(parent=self)
|
self._material_sync = CloudMaterialSync(parent=self)
|
||||||
self._checkIfNewMaterialsWereInstalled()
|
|
||||||
|
|
||||||
def _checkIfNewMaterialsWereInstalled(self) -> None:
|
|
||||||
"""
|
|
||||||
Checks whether new material packages were installed in the latest startup. If there were, then it shows
|
|
||||||
a message prompting the user to sync the materials with their printers.
|
|
||||||
"""
|
|
||||||
application = cura.CuraApplication.CuraApplication.getInstance()
|
|
||||||
for package_id, package_data in application.getPackageManager().getPackagesInstalledOnStartup().items():
|
|
||||||
if package_data["package_info"]["package_type"] == "material":
|
|
||||||
# At least one new material was installed
|
|
||||||
# TODO: This should be enabled again once CURA-8609 is merged
|
|
||||||
#self._showSyncNewMaterialsMessage()
|
|
||||||
break
|
|
||||||
|
|
||||||
def _showSyncNewMaterialsMessage(self) -> None:
|
|
||||||
sync_materials_message = Message(
|
|
||||||
text = catalog.i18nc("@action:button",
|
|
||||||
"Please sync the material profiles with your printers before starting to print."),
|
|
||||||
title = catalog.i18nc("@action:button", "New materials installed"),
|
|
||||||
message_type = Message.MessageType.WARNING,
|
|
||||||
lifetime = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
sync_materials_message.addAction(
|
|
||||||
"sync",
|
|
||||||
name = catalog.i18nc("@action:button", "Sync materials"),
|
|
||||||
icon = "",
|
|
||||||
description = "Sync your newly installed materials with your printers.",
|
|
||||||
button_align = Message.ActionButtonAlignment.ALIGN_RIGHT
|
|
||||||
)
|
|
||||||
|
|
||||||
sync_materials_message.addAction(
|
|
||||||
"learn_more",
|
|
||||||
name = catalog.i18nc("@action:button", "Learn more"),
|
|
||||||
icon = "",
|
|
||||||
description = "Learn more about syncing your newly installed materials with your printers.",
|
|
||||||
button_align = Message.ActionButtonAlignment.ALIGN_LEFT,
|
|
||||||
button_style = Message.ActionButtonStyle.LINK
|
|
||||||
)
|
|
||||||
sync_materials_message.actionTriggered.connect(self._onSyncMaterialsMessageActionTriggered)
|
|
||||||
|
|
||||||
# Show the message only if there are printers that support material export
|
|
||||||
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
|
|
||||||
global_stacks = container_registry.findContainerStacks(type = "machine")
|
|
||||||
if any([stack.supportsMaterialExport for stack in global_stacks]):
|
|
||||||
sync_materials_message.show()
|
|
||||||
|
|
||||||
def _onSyncMaterialsMessageActionTriggered(self, sync_message: Message, sync_message_action: str):
|
|
||||||
if sync_message_action == "sync":
|
|
||||||
QDesktopServices.openUrl(QUrl("https://example.com/openSyncAllWindow"))
|
|
||||||
# self.openSyncAllWindow()
|
|
||||||
sync_message.hide()
|
|
||||||
elif sync_message_action == "learn_more":
|
|
||||||
QDesktopServices.openUrl(QUrl("https://support.ultimaker.com/hc/en-us/articles/360013137919?utm_source=cura&utm_medium=software&utm_campaign=sync-material-printer-message"))
|
|
||||||
|
|
||||||
|
|
||||||
@pyqtSlot("QVariant", result = bool)
|
@pyqtSlot("QVariant", result = bool)
|
||||||
def canMaterialBeRemoved(self, material_node: "MaterialNode") -> bool:
|
def canMaterialBeRemoved(self, material_node: "MaterialNode") -> bool:
|
||||||
|
|
|
@ -358,8 +358,9 @@ class QualityManagementModel(ListModel):
|
||||||
"quality_type": quality_type,
|
"quality_type": quality_type,
|
||||||
"quality_changes_group": None,
|
"quality_changes_group": None,
|
||||||
"intent_category": intent_category,
|
"intent_category": intent_category,
|
||||||
"section_name": catalog.i18nc("@label", intent_translations.get(intent_category, {}).get("name", catalog.i18nc("@label", "Unknown"))),
|
"section_name": catalog.i18nc("@label", intent_translations.get(intent_category, {}).get("name", catalog.i18nc("@label", intent_category.title()))),
|
||||||
})
|
})
|
||||||
|
|
||||||
# Sort by quality_type for each intent category
|
# Sort by quality_type for each intent category
|
||||||
intent_translations_list = list(intent_translations)
|
intent_translations_list = list(intent_translations)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2022 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import Optional, TYPE_CHECKING, List
|
from typing import Optional, TYPE_CHECKING, List
|
||||||
|
@ -6,6 +6,8 @@ from typing import Optional, TYPE_CHECKING, List
|
||||||
from PyQt6.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot, QUrl
|
from PyQt6.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot, QUrl
|
||||||
from PyQt6.QtGui import QImage
|
from PyQt6.QtGui import QImage
|
||||||
|
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||||
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
|
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
|
||||||
|
@ -86,6 +88,18 @@ class PrintJobOutputModel(QObject):
|
||||||
self._owner = owner
|
self._owner = owner
|
||||||
self.ownerChanged.emit()
|
self.ownerChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = ownerChanged)
|
||||||
|
def isMine(self) -> bool:
|
||||||
|
"""
|
||||||
|
Returns whether this print job was sent by the currently logged in user.
|
||||||
|
|
||||||
|
This checks the owner of the print job with the owner of the currently
|
||||||
|
logged in account. Both of these are human-readable account names which
|
||||||
|
may be duplicate. In practice the harm here is limited, but it's the
|
||||||
|
best we can do with the information available to the API.
|
||||||
|
"""
|
||||||
|
return self._owner == CuraApplication.getInstance().getCuraAPI().account.userName
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify=assignedPrinterChanged)
|
@pyqtProperty(QObject, notify=assignedPrinterChanged)
|
||||||
def assignedPrinter(self):
|
def assignedPrinter(self):
|
||||||
return self._assigned_printer
|
return self._assigned_printer
|
||||||
|
@ -119,16 +133,16 @@ class PrintJobOutputModel(QObject):
|
||||||
|
|
||||||
@pyqtProperty(int, notify = timeTotalChanged)
|
@pyqtProperty(int, notify = timeTotalChanged)
|
||||||
def timeTotal(self) -> int:
|
def timeTotal(self) -> int:
|
||||||
return self._time_total
|
return int(self._time_total)
|
||||||
|
|
||||||
@pyqtProperty(int, notify = timeElapsedChanged)
|
@pyqtProperty(int, notify = timeElapsedChanged)
|
||||||
def timeElapsed(self) -> int:
|
def timeElapsed(self) -> int:
|
||||||
return self._time_elapsed
|
return int(self._time_elapsed)
|
||||||
|
|
||||||
@pyqtProperty(int, notify = timeElapsedChanged)
|
@pyqtProperty(int, notify = timeElapsedChanged)
|
||||||
def timeRemaining(self) -> int:
|
def timeRemaining(self) -> int:
|
||||||
# Never get a negative time remaining
|
# Never get a negative time remaining
|
||||||
return max(self.timeTotal - self.timeElapsed, 0)
|
return int(max(self.timeTotal - self.timeElapsed, 0))
|
||||||
|
|
||||||
@pyqtProperty(float, notify = timeElapsedChanged)
|
@pyqtProperty(float, notify = timeElapsedChanged)
|
||||||
def progress(self) -> float:
|
def progress(self) -> float:
|
||||||
|
|
|
@ -114,7 +114,7 @@ class ContainerManager(QObject):
|
||||||
for _ in range(len(entries)):
|
for _ in range(len(entries)):
|
||||||
item = item.get(entries.pop(0), {})
|
item = item.get(entries.pop(0), {})
|
||||||
|
|
||||||
if item[entry_name] != entry_value:
|
if entry_name not in item or item[entry_name] != entry_value:
|
||||||
sub_item_changed = True
|
sub_item_changed = True
|
||||||
item[entry_name] = entry_value
|
item[entry_name] = entry_value
|
||||||
|
|
||||||
|
@ -206,7 +206,7 @@ class ContainerManager(QObject):
|
||||||
if os.path.exists(file_url):
|
if os.path.exists(file_url):
|
||||||
result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
|
result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
|
||||||
catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_url))
|
catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_url))
|
||||||
if result == QMessageBox.ButtonRole.NoRole:
|
if result == QMessageBox.StandardButton.No:
|
||||||
return {"status": "cancelled", "message": "User cancelled"}
|
return {"status": "cancelled", "message": "User cancelled"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -139,7 +139,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
if os.path.exists(file_name):
|
if os.path.exists(file_name):
|
||||||
result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
|
result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
|
||||||
catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name))
|
catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name))
|
||||||
if result == QMessageBox.ButtonRole.NoRole:
|
if result == QMessageBox.StandardButton.No:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
profile_writer = self._findProfileWriter(extension, description)
|
profile_writer = self._findProfileWriter(extension, description)
|
||||||
|
|
|
@ -17,7 +17,7 @@ class CuraStackBuilder:
|
||||||
"""Contains helper functions to create new machines."""
|
"""Contains helper functions to create new machines."""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def createMachine(cls, name: str, definition_id: str, machine_extruder_count: Optional[int] = None) -> Optional[GlobalStack]:
|
def createMachine(cls, name: str, definition_id: str, machine_extruder_count: Optional[int] = None, show_warning_message: bool = True) -> Optional[GlobalStack]:
|
||||||
"""Create a new instance of a machine.
|
"""Create a new instance of a machine.
|
||||||
|
|
||||||
:param name: The name of the new machine.
|
:param name: The name of the new machine.
|
||||||
|
@ -34,7 +34,8 @@ class CuraStackBuilder:
|
||||||
|
|
||||||
definitions = registry.findDefinitionContainers(id = definition_id)
|
definitions = registry.findDefinitionContainers(id = definition_id)
|
||||||
if not definitions:
|
if not definitions:
|
||||||
ConfigurationErrorMessage.getInstance().addFaultyContainers(definition_id)
|
if show_warning_message:
|
||||||
|
ConfigurationErrorMessage.getInstance().addFaultyContainers(definition_id)
|
||||||
Logger.log("w", "Definition {definition} was not found!", definition = definition_id)
|
Logger.log("w", "Definition {definition} was not found!", definition = definition_id)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (c) 2020 Ultimaker B.V.
|
# Copyright (c) 2022 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt6.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt.
|
from PyQt6.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt.
|
||||||
|
@ -382,7 +382,10 @@ class ExtruderManager(QObject):
|
||||||
# "fdmextruder". We need to check a machine here so its extruder definition is correct according to this.
|
# "fdmextruder". We need to check a machine here so its extruder definition is correct according to this.
|
||||||
def fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None:
|
def fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None:
|
||||||
container_registry = ContainerRegistry.getInstance()
|
container_registry = ContainerRegistry.getInstance()
|
||||||
expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"]
|
expected_extruder_stack = global_stack.getMetaDataEntry("machine_extruder_trains")
|
||||||
|
if expected_extruder_stack is None:
|
||||||
|
return
|
||||||
|
expected_extruder_definition_0_id = expected_extruder_stack["0"]
|
||||||
try:
|
try:
|
||||||
extruder_stack_0 = global_stack.extruderList[0]
|
extruder_stack_0 = global_stack.extruderList[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (c) 2019 Ultimaker B.V.
|
# Copyright (c) 2022 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
|
from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
|
||||||
|
@ -8,6 +8,7 @@ from UM.Logger import Logger
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
|
|
||||||
import cura.CuraApplication
|
import cura.CuraApplication
|
||||||
|
from UM.Signal import Signal
|
||||||
from cura.Machines.ContainerTree import ContainerTree
|
from cura.Machines.ContainerTree import ContainerTree
|
||||||
from cura.Settings.cura_empty_instance_containers import empty_intent_container
|
from cura.Settings.cura_empty_instance_containers import empty_intent_container
|
||||||
|
|
||||||
|
@ -29,6 +30,7 @@ class IntentManager(QObject):
|
||||||
return cls.__instance
|
return cls.__instance
|
||||||
|
|
||||||
intentCategoryChanged = pyqtSignal() #Triggered when we switch categories.
|
intentCategoryChanged = pyqtSignal() #Triggered when we switch categories.
|
||||||
|
intentCategoryChangedSignal = Signal()
|
||||||
|
|
||||||
def intentMetadatas(self, definition_id: str, nozzle_name: str, material_base_file: str) -> List[Dict[str, Any]]:
|
def intentMetadatas(self, definition_id: str, nozzle_name: str, material_base_file: str) -> List[Dict[str, Any]]:
|
||||||
"""Gets the metadata dictionaries of all intent profiles for a given
|
"""Gets the metadata dictionaries of all intent profiles for a given
|
||||||
|
@ -189,3 +191,4 @@ class IntentManager(QObject):
|
||||||
application.getMachineManager().setQualityGroupByQualityType(quality_type)
|
application.getMachineManager().setQualityGroupByQualityType(quality_type)
|
||||||
if old_intent_category != intent_category:
|
if old_intent_category != intent_category:
|
||||||
self.intentCategoryChanged.emit()
|
self.intentCategoryChanged.emit()
|
||||||
|
self.intentCategoryChangedSignal.emit()
|
||||||
|
|
|
@ -1611,7 +1611,7 @@ class MachineManager(QObject):
|
||||||
if intent_category != "default":
|
if intent_category != "default":
|
||||||
intent_display_name = IntentCategoryModel.translation(intent_category,
|
intent_display_name = IntentCategoryModel.translation(intent_category,
|
||||||
"name",
|
"name",
|
||||||
catalog.i18nc("@label", "Unknown"))
|
intent_category.title())
|
||||||
display_name = "{intent_name} - {the_rest}".format(intent_name = intent_display_name,
|
display_name = "{intent_name} - {the_rest}".format(intent_name = intent_display_name,
|
||||||
the_rest = display_name)
|
the_rest = display_name)
|
||||||
|
|
||||||
|
@ -1778,3 +1778,31 @@ class MachineManager(QObject):
|
||||||
abbr_machine += stripped_word
|
abbr_machine += stripped_word
|
||||||
|
|
||||||
return abbr_machine
|
return abbr_machine
|
||||||
|
|
||||||
|
@pyqtSlot(str, str, result = bool)
|
||||||
|
def intentCategoryHasQuality(self, intent_category: str, quality_type: str) -> bool:
|
||||||
|
""" Checks if there are any quality groups for active extruders that have an intent category """
|
||||||
|
quality_groups = ContainerTree.getInstance().getCurrentQualityGroups()
|
||||||
|
|
||||||
|
if quality_type in quality_groups:
|
||||||
|
quality_group = quality_groups[quality_type]
|
||||||
|
for node in quality_group.nodes_for_extruders.values():
|
||||||
|
if any(intent.intent_category == intent_category for intent in node.intents.values()):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
@pyqtSlot(str, result = str)
|
||||||
|
def getDefaultQualityTypeForIntent(self, intent_category) -> str:
|
||||||
|
""" If there is an intent category for the default machine quality return it, otherwise return the first quality for this intent category """
|
||||||
|
machine = ContainerTree.getInstance().machines.get(self._global_container_stack.definition.getId())
|
||||||
|
|
||||||
|
if self.intentCategoryHasQuality(intent_category, machine.preferred_quality_type):
|
||||||
|
return machine.preferred_quality_type
|
||||||
|
|
||||||
|
for quality_type, quality_group in ContainerTree.getInstance().getCurrentQualityGroups().items():
|
||||||
|
for node in quality_group.nodes_for_extruders.values():
|
||||||
|
if any(intent.intent_category == intent_category for intent in node.intents.values()):
|
||||||
|
return quality_type
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
|
@ -29,7 +29,7 @@ class SingleInstance:
|
||||||
single_instance_socket.connectToServer("ultimaker-cura")
|
single_instance_socket.connectToServer("ultimaker-cura")
|
||||||
single_instance_socket.waitForConnected(msecs = 3000) # wait for 3 seconds
|
single_instance_socket.waitForConnected(msecs = 3000) # wait for 3 seconds
|
||||||
|
|
||||||
if single_instance_socket.state() != QLocalSocket.ConnectedState:
|
if single_instance_socket.state() != QLocalSocket.LocalSocketState.ConnectedState:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# We only send the files that need to be opened.
|
# We only send the files that need to be opened.
|
||||||
|
@ -37,7 +37,7 @@ class SingleInstance:
|
||||||
Logger.log("i", "No file need to be opened, do nothing.")
|
Logger.log("i", "No file need to be opened, do nothing.")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if single_instance_socket.state() == QLocalSocket.ConnectedState:
|
if single_instance_socket.state() == QLocalSocket.LocalSocketState.ConnectedState:
|
||||||
Logger.log("i", "Connection has been made to the single-instance Cura socket.")
|
Logger.log("i", "Connection has been made to the single-instance Cura socket.")
|
||||||
|
|
||||||
# Protocol is one line of JSON terminated with a carriage return.
|
# Protocol is one line of JSON terminated with a carriage return.
|
||||||
|
|
|
@ -38,6 +38,8 @@ class PrintInformation(QObject):
|
||||||
|
|
||||||
self.initializeCuraMessagePrintTimeProperties()
|
self.initializeCuraMessagePrintTimeProperties()
|
||||||
|
|
||||||
|
self.slice_uuid: Optional[str] = None
|
||||||
|
|
||||||
# Indexed by build plate number
|
# Indexed by build plate number
|
||||||
self._material_lengths = {} # type: Dict[int, List[float]]
|
self._material_lengths = {} # type: Dict[int, List[float]]
|
||||||
self._material_weights = {} # type: Dict[int, List[float]]
|
self._material_weights = {} # type: Dict[int, List[float]]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (c) 2021 Ultimaker B.V.
|
# Copyright (c) 2022 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl
|
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl
|
||||||
|
@ -18,6 +18,7 @@ if TYPE_CHECKING:
|
||||||
from UM.Signal import Signal
|
from UM.Signal import Signal
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
class CloudMaterialSync(QObject):
|
class CloudMaterialSync(QObject):
|
||||||
"""
|
"""
|
||||||
Handles the synchronisation of material profiles with cloud accounts.
|
Handles the synchronisation of material profiles with cloud accounts.
|
||||||
|
@ -44,7 +45,6 @@ class CloudMaterialSync(QObject):
|
||||||
break
|
break
|
||||||
|
|
||||||
def openSyncAllWindow(self):
|
def openSyncAllWindow(self):
|
||||||
|
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
if self.sync_all_dialog is None:
|
if self.sync_all_dialog is None:
|
||||||
|
|
|
@ -18,6 +18,7 @@ import os
|
||||||
if sys.platform != "linux": # Turns out the Linux build _does_ use this, but we're not making an Enterprise release for that system anyway.
|
if sys.platform != "linux": # Turns out the Linux build _does_ use this, but we're not making an Enterprise release for that system anyway.
|
||||||
os.environ["QT_PLUGIN_PATH"] = "" # Security workaround: Don't need it, and introduces an attack vector, so set to nul.
|
os.environ["QT_PLUGIN_PATH"] = "" # Security workaround: Don't need it, and introduces an attack vector, so set to nul.
|
||||||
os.environ["QML2_IMPORT_PATH"] = "" # Security workaround: Don't need it, and introduces an attack vector, so set to nul.
|
os.environ["QML2_IMPORT_PATH"] = "" # Security workaround: Don't need it, and introduces an attack vector, so set to nul.
|
||||||
|
os.environ["QT_OPENGL_DLL"] = "" # Security workaround: Don't need it, and introduces an attack vector, so set to nul.
|
||||||
|
|
||||||
from PyQt6.QtNetwork import QSslConfiguration, QSslSocket
|
from PyQt6.QtNetwork import QSslConfiguration, QSslSocket
|
||||||
|
|
||||||
|
|
|
@ -1,71 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# Abort at the first error.
|
|
||||||
set -e
|
|
||||||
|
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
|
||||||
PROJECT_DIR="$( cd "${SCRIPT_DIR}/.." && pwd )"
|
|
||||||
|
|
||||||
# Make sure that environment variables are set properly
|
|
||||||
export PATH="${CURA_BUILD_ENV_PATH}/bin:${PATH}"
|
|
||||||
export PKG_CONFIG_PATH="${CURA_BUILD_ENV_PATH}/lib/pkgconfig:${PKG_CONFIG_PATH}"
|
|
||||||
export LD_LIBRARY_PATH="${CURA_BUILD_ENV_PATH}/lib:${LD_LIBRARY_PATH}"
|
|
||||||
|
|
||||||
cd "${PROJECT_DIR}"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Clone Uranium and set PYTHONPATH first
|
|
||||||
#
|
|
||||||
|
|
||||||
# Check the branch to use for Uranium.
|
|
||||||
# It tries the following branch names and uses the first one that's available.
|
|
||||||
# - GITHUB_HEAD_REF: the branch name of a PR. If it's not a PR, it will be empty.
|
|
||||||
# - GITHUB_BASE_REF: the branch a PR is based on. If it's not a PR, it will be empty.
|
|
||||||
# - GITHUB_REF: the branch name if it's a branch on the repository;
|
|
||||||
# refs/pull/123/merge if it's a pull_request.
|
|
||||||
# - master: the master branch. It should always exist.
|
|
||||||
|
|
||||||
# For debugging.
|
|
||||||
echo "GITHUB_REF: ${GITHUB_REF}"
|
|
||||||
echo "GITHUB_HEAD_REF: ${GITHUB_HEAD_REF}"
|
|
||||||
echo "GITHUB_BASE_REF: ${GITHUB_BASE_REF}"
|
|
||||||
|
|
||||||
GIT_REF_NAME_LIST=( "${GITHUB_HEAD_REF}" "${GITHUB_BASE_REF}" "${GITHUB_REF}" "master" )
|
|
||||||
for git_ref_name in "${GIT_REF_NAME_LIST[@]}"
|
|
||||||
do
|
|
||||||
if [ -z "${git_ref_name}" ]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
git_ref_name="$(basename "${git_ref_name}")"
|
|
||||||
# Skip refs/pull/1234/merge as pull requests use it as GITHUB_REF
|
|
||||||
if [[ "${git_ref_name}" == "merge" ]]; then
|
|
||||||
echo "Skip [${git_ref_name}]"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
URANIUM_BRANCH="${git_ref_name}"
|
|
||||||
output="$(git ls-remote --heads https://github.com/Ultimaker/Uranium.git "${URANIUM_BRANCH}")"
|
|
||||||
if [ -n "${output}" ]; then
|
|
||||||
echo "Found Uranium branch [${URANIUM_BRANCH}]."
|
|
||||||
break
|
|
||||||
else
|
|
||||||
echo "Could not find Uranium branch [${URANIUM_BRANCH}], try next."
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Using Uranium branch ${URANIUM_BRANCH} ..."
|
|
||||||
git clone --depth=1 -b "${URANIUM_BRANCH}" https://github.com/Ultimaker/Uranium.git "${PROJECT_DIR}"/Uranium
|
|
||||||
export PYTHONPATH="${PROJECT_DIR}/Uranium:.:${PYTHONPATH}"
|
|
||||||
|
|
||||||
mkdir build
|
|
||||||
cd build
|
|
||||||
cmake \
|
|
||||||
-DCMAKE_BUILD_TYPE=Debug \
|
|
||||||
-DCMAKE_PREFIX_PATH="${CURA_BUILD_ENV_PATH}" \
|
|
||||||
-DURANIUM_DIR="${PROJECT_DIR}/Uranium" \
|
|
||||||
-DBUILD_TESTS=ON \
|
|
||||||
-DPRINT_PLUGIN_LIST=OFF \
|
|
||||||
-DGENERATE_TRANSLATIONS=OFF \
|
|
||||||
..
|
|
||||||
make
|
|
|
@ -1,3 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
cd build
|
|
||||||
ctest -j4 --output-on-failure -T Test
|
|
54
docs/resources/deps.dot
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
digraph {
|
||||||
|
"cpython/3.10.4@ultimaker/testing" -> "zlib/1.2.12"
|
||||||
|
"cpython/3.10.4@ultimaker/testing" -> "openssl/1.1.1l"
|
||||||
|
"cpython/3.10.4@ultimaker/testing" -> "expat/2.4.1"
|
||||||
|
"cpython/3.10.4@ultimaker/testing" -> "libffi/3.2.1"
|
||||||
|
"cpython/3.10.4@ultimaker/testing" -> "mpdecimal/2.5.0@ultimaker/testing"
|
||||||
|
"cpython/3.10.4@ultimaker/testing" -> "libuuid/1.0.3"
|
||||||
|
"cpython/3.10.4@ultimaker/testing" -> "libxcrypt/4.4.25"
|
||||||
|
"cpython/3.10.4@ultimaker/testing" -> "bzip2/1.0.8"
|
||||||
|
"cpython/3.10.4@ultimaker/testing" -> "gdbm/1.19"
|
||||||
|
"cpython/3.10.4@ultimaker/testing" -> "sqlite3/3.36.0"
|
||||||
|
"cpython/3.10.4@ultimaker/testing" -> "tk/8.6.10"
|
||||||
|
"cpython/3.10.4@ultimaker/testing" -> "ncurses/6.2"
|
||||||
|
"cpython/3.10.4@ultimaker/testing" -> "xz_utils/5.2.5"
|
||||||
|
"pynest2d/5.1.0-beta+3@ultimaker/stable" -> "libnest2d/5.1.0-beta+3@ultimaker/stable"
|
||||||
|
"pynest2d/5.1.0-beta+3@ultimaker/stable" -> "cpython/3.10.4@ultimaker/testing"
|
||||||
|
"freetype/2.12.1" -> "libpng/1.6.37"
|
||||||
|
"freetype/2.12.1" -> "zlib/1.2.12"
|
||||||
|
"freetype/2.12.1" -> "bzip2/1.0.8"
|
||||||
|
"freetype/2.12.1" -> "brotli/1.0.9"
|
||||||
|
"savitar/5.1.0-beta+3@ultimaker/stable" -> "pugixml/1.12.1"
|
||||||
|
"savitar/5.1.0-beta+3@ultimaker/stable" -> "cpython/3.10.4@ultimaker/testing"
|
||||||
|
"arcus/5.1.0-beta+3@ultimaker/stable" -> "protobuf/3.17.1"
|
||||||
|
"arcus/5.1.0-beta+3@ultimaker/stable" -> "cpython/3.10.4@ultimaker/testing"
|
||||||
|
"arcus/5.1.0-beta+3@ultimaker/stable" -> "zlib/1.2.12"
|
||||||
|
"libpng/1.6.37" -> "zlib/1.2.12"
|
||||||
|
"curaengine/5.1.0-beta+3@ultimaker/stable" -> "clipper/6.4.2"
|
||||||
|
"curaengine/5.1.0-beta+3@ultimaker/stable" -> "boost/1.78.0"
|
||||||
|
"curaengine/5.1.0-beta+3@ultimaker/stable" -> "rapidjson/1.1.0"
|
||||||
|
"curaengine/5.1.0-beta+3@ultimaker/stable" -> "stb/20200203"
|
||||||
|
"curaengine/5.1.0-beta+3@ultimaker/stable" -> "protobuf/3.17.1"
|
||||||
|
"curaengine/5.1.0-beta+3@ultimaker/stable" -> "arcus/5.1.0-beta+3@ultimaker/stable"
|
||||||
|
"tcl/8.6.10" -> "zlib/1.2.12"
|
||||||
|
"uranium/5.1.0-beta+3@ultimaker/stable" -> "arcus/5.1.0-beta+3@ultimaker/stable"
|
||||||
|
"uranium/5.1.0-beta+3@ultimaker/stable" -> "cpython/3.10.4@ultimaker/testing"
|
||||||
|
"libnest2d/5.1.0-beta+3@ultimaker/stable" -> "boost/1.78.0"
|
||||||
|
"libnest2d/5.1.0-beta+3@ultimaker/stable" -> "clipper/6.4.2"
|
||||||
|
"libnest2d/5.1.0-beta+3@ultimaker/stable" -> "nlopt/2.7.0"
|
||||||
|
"conanfile.py (cura/5.1.0-beta+3@ultimaker/testing)" -> "arcus/5.1.0-beta+3@ultimaker/stable"
|
||||||
|
"conanfile.py (cura/5.1.0-beta+3@ultimaker/testing)" -> "curaengine/5.1.0-beta+3@ultimaker/stable"
|
||||||
|
"conanfile.py (cura/5.1.0-beta+3@ultimaker/testing)" -> "savitar/5.1.0-beta+3@ultimaker/stable"
|
||||||
|
"conanfile.py (cura/5.1.0-beta+3@ultimaker/testing)" -> "pynest2d/5.1.0-beta+3@ultimaker/stable"
|
||||||
|
"conanfile.py (cura/5.1.0-beta+3@ultimaker/testing)" -> "uranium/5.1.0-beta+3@ultimaker/stable"
|
||||||
|
"conanfile.py (cura/5.1.0-beta+3@ultimaker/testing)" -> "fdm_materials/5.1.0-beta+3@ultimaker/stable"
|
||||||
|
"conanfile.py (cura/5.1.0-beta+3@ultimaker/testing)" -> "cura_binary_data/5.1.0-beta+3@ultimaker/stable"
|
||||||
|
"conanfile.py (cura/5.1.0-beta+3@ultimaker/testing)" -> "cpython/3.10.4@ultimaker/testing"
|
||||||
|
"fontconfig/2.13.93" -> "freetype/2.12.1"
|
||||||
|
"fontconfig/2.13.93" -> "expat/2.4.1"
|
||||||
|
"fontconfig/2.13.93" -> "libuuid/1.0.3"
|
||||||
|
"tk/8.6.10" -> "tcl/8.6.10"
|
||||||
|
"tk/8.6.10" -> "fontconfig/2.13.93"
|
||||||
|
"tk/8.6.10" -> "xorg/system"
|
||||||
|
"protobuf/3.17.1" -> "zlib/1.2.12"
|
||||||
|
}
|
BIN
icons/cura.icns
20
packaging/AppImage/AppRun
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
scriptdir=$(dirname $0)
|
||||||
|
|
||||||
|
export PYTHONPATH="$scriptdir/lib/python3.10"
|
||||||
|
export LD_LIBRARY_PATH=$scriptdir
|
||||||
|
export QT_PLUGIN_PATH="$scriptdir/qt/plugins"
|
||||||
|
export QML2_IMPORT_PATH="$scriptdir/qt/qml"
|
||||||
|
export QT_QPA_FONTDIR=/usr/share/fonts
|
||||||
|
export QT_QPA_PLATFORMTHEME=xdgdesktopportal
|
||||||
|
export QT_XKB_CONFIG_ROOT=/usr/share/X11/xkb
|
||||||
|
|
||||||
|
# Use the openssl.cnf packaged in the AppImage
|
||||||
|
export OPENSSL_CONF="$scriptdir/openssl.cnf"
|
||||||
|
|
||||||
|
$scriptdir/Ultimaker-Cura "$@"
|
||||||
|
|
||||||
|
# If this variable is set on Zorin OS 16 Cura would crash
|
||||||
|
# unset `QT_STYLE_OVERRIDE` as a precaution
|
||||||
|
unset QT_STYLE_OVERRIDE
|
76
packaging/AppImage/create_appimage.py
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
# Copyright (c) 2022 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import argparse # Command line arguments parsing and help.
|
||||||
|
from jinja2 import Template
|
||||||
|
import os # Finding installation directory.
|
||||||
|
import os.path # Finding files.
|
||||||
|
import shutil # Copying files.
|
||||||
|
import stat # For setting file permissions.
|
||||||
|
import subprocess # For calling system commands.
|
||||||
|
|
||||||
|
def build_appimage(dist_path, version, appimage_filename):
|
||||||
|
"""
|
||||||
|
Creates an AppImage file from the build artefacts created so far.
|
||||||
|
"""
|
||||||
|
copy_metadata_files(dist_path, version)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.remove(os.path.join(dist_path, appimage_filename)) # Ensure any old file is removed, if it exists.
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass # If it didn't exist, that's even better.
|
||||||
|
|
||||||
|
generate_appimage(dist_path, appimage_filename)
|
||||||
|
|
||||||
|
sign_appimage(dist_path, appimage_filename)
|
||||||
|
|
||||||
|
def copy_metadata_files(dist_path, version):
|
||||||
|
"""
|
||||||
|
Copy metadata files for the metadata of the AppImage.
|
||||||
|
"""
|
||||||
|
copied_files = {
|
||||||
|
os.path.join("..", "icons", "cura-icon_256x256.png"): "cura-icon.png",
|
||||||
|
"cura.appdata.xml": "cura.appdata.xml",
|
||||||
|
"AppRun": "AppRun"
|
||||||
|
}
|
||||||
|
|
||||||
|
packaging_dir = os.path.dirname(__file__)
|
||||||
|
for source, dest in copied_files.items():
|
||||||
|
print("Copying", os.path.join(packaging_dir, source), "to", os.path.join(dist_path, dest))
|
||||||
|
shutil.copyfile(os.path.join(packaging_dir, source), os.path.join(dist_path, dest))
|
||||||
|
|
||||||
|
# Ensure that AppRun has the proper permissions: 755 (user reads, writes and executes, group reads and executes, world reads and executes).
|
||||||
|
print("Changing permissions for AppRun")
|
||||||
|
os.chmod(os.path.join(dist_path, "AppRun"), stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
|
||||||
|
|
||||||
|
# Provision the Desktop file with the correct version number.
|
||||||
|
template_path = os.path.join(packaging_dir, "cura.desktop.jinja")
|
||||||
|
desktop_path = os.path.join(dist_path, "cura.desktop")
|
||||||
|
print("Provisioning desktop file from", template_path, "to", desktop_path)
|
||||||
|
with open(template_path, "r") as f:
|
||||||
|
desktop_file = Template(f.read())
|
||||||
|
with open(desktop_path, "w") as f:
|
||||||
|
f.write(desktop_file.render(cura_version = version))
|
||||||
|
|
||||||
|
def generate_appimage(dist_path, appimage_filename):
|
||||||
|
appimage_path = os.path.join(dist_path, "..", appimage_filename)
|
||||||
|
appimagetool = os.getenv("APPIMAGETOOL_LOCATION", "appimagetool")
|
||||||
|
command = [appimagetool, "--appimage-extract-and-run", f"{dist_path}/", appimage_path]
|
||||||
|
result = subprocess.call(command)
|
||||||
|
if result != 0:
|
||||||
|
raise RuntimeError(f"The AppImageTool command returned non-zero: {result}")
|
||||||
|
|
||||||
|
def sign_appimage(dist_path, appimage_filename):
|
||||||
|
appimage_path = os.path.join(dist_path, "..", appimage_filename)
|
||||||
|
command = ["gpg", "--yes", "--armor", "--detach-sig", appimage_path]
|
||||||
|
result = subprocess.call(command)
|
||||||
|
if result != 0:
|
||||||
|
raise RuntimeError(f"The GPG command returned non-zero: {result}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description = "Create AppImages of Cura.")
|
||||||
|
parser.add_argument("dist_path", type=str, help="Path to where PyInstaller installed the distribution of Cura.")
|
||||||
|
parser.add_argument("version", type=str, help="Full version number of Cura (e.g. '5.1.0-beta')")
|
||||||
|
parser.add_argument("filename", type = str, help = "Filename of the AppImage (e.g. 'Ultimaker-Cura-5.1.0-beta-Linux-X64.AppImage')")
|
||||||
|
args = parser.parse_args()
|
||||||
|
build_appimage(args.dist_path, args.version, args.filename)
|
18
packaging/AppImage/cura.appdata.xml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<component type="desktop-application">
|
||||||
|
<id>com.ultimaker.cura</id>
|
||||||
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
|
<project_license>LGPL-3.0</project_license>
|
||||||
|
<name>Ultimaker Cura</name>
|
||||||
|
<summary>Slicer to prepare your 3D printing projects</summary>
|
||||||
|
<description>
|
||||||
|
<p>Ultimaker Cura is a slicer, an application that prepares your model for 3D printing. Optimized, expert-tested profiles for 3D printers and materials mean you can start printing reliably in no time. And with industry-standard software integration, you can streamline your workflow for maximum efficiency.</p>
|
||||||
|
</description>
|
||||||
|
<url type="homepage">https://ultimaker.com/en/software/ultimaker-cura</url>
|
||||||
|
<screenshots>
|
||||||
|
<screenshot type="default">
|
||||||
|
<caption>Print preparation screen</caption>
|
||||||
|
<image>https://raw.githubusercontent.com/Ultimaker/Cura/master/screenshot.png</image>
|
||||||
|
</screenshot>
|
||||||
|
</screenshots>
|
||||||
|
</component>
|
15
packaging/AppImage/cura.desktop.jinja
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=Ultimaker Cura
|
||||||
|
Name[de]=Ultimaker Cura
|
||||||
|
GenericName=3D Printing Software
|
||||||
|
GenericName[de]=3D-Druck-Software
|
||||||
|
GenericName[nl]=3D-Print Software
|
||||||
|
Comment=Cura converts 3D models into paths for a 3D printer. It prepares your print for maximum accuracy, minimum printing time and good reliability with many extra features that make your print come out great.
|
||||||
|
Exec=Ultimaker-Cura %F
|
||||||
|
Icon=cura-icon
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;text/x-gcode;application/x-amf;application/x-ply;application/x-ctm;model/vnd.collada+xml;model/gltf-binary;model/gltf+json;model/vnd.collada+xml+zip;
|
||||||
|
Categories=Graphics;
|
||||||
|
Keywords=3D;Printing;
|
||||||
|
X-AppImage-Version={{ cura_version }}
|
194
packaging/NSIS/Ultimaker-Cura.nsi.jinja
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
# Copyright (c) 2022 Ultimaker B.V.
|
||||||
|
# Cura's build system is released under the terms of the AGPLv3 or higher.
|
||||||
|
|
||||||
|
!define APP_NAME "{{ app_name }}"
|
||||||
|
!define COMP_NAME "{{ company }}"
|
||||||
|
!define WEB_SITE "{{ web_site }}"
|
||||||
|
!define VERSION "{{ version }}"
|
||||||
|
!define VIVERSION "{{ version_major }}.{{ version_minor }}.{{ version_patch }}.0"
|
||||||
|
!define COPYRIGHT "Copyright (c) {{ year }} {{ company }}"
|
||||||
|
!define DESCRIPTION "Application"
|
||||||
|
!define LICENSE_TXT "{{ cura_license_file }}"
|
||||||
|
!define INSTALLER_NAME "{{ destination }}"
|
||||||
|
!define MAIN_APP_EXE "{{ main_app }}"
|
||||||
|
!define INSTALL_TYPE "SetShellVarContext all"
|
||||||
|
!define REG_ROOT "HKCR"
|
||||||
|
!define REG_APP_PATH "Software\Microsoft\Windows\CurrentVersion\App Paths\${APP_NAME}"
|
||||||
|
!define UNINSTALL_PATH "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}"
|
||||||
|
|
||||||
|
!define REG_START_MENU "Start Menu Folder"
|
||||||
|
|
||||||
|
;Require administrator access
|
||||||
|
RequestExecutionLevel admin
|
||||||
|
|
||||||
|
var SM_Folder
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
VIProductVersion "${VIVERSION}"
|
||||||
|
VIAddVersionKey "ProductName" "{{ app_name }}"
|
||||||
|
VIAddVersionKey "CompanyName" "${COMP_NAME}"
|
||||||
|
VIAddVersionKey "LegalCopyright" "${COPYRIGHT}"
|
||||||
|
VIAddVersionKey "FileDescription" "${DESCRIPTION}"
|
||||||
|
VIAddVersionKey "FileVersion" "${VIVERSION}"
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
SetCompressor {{ compression_method }}
|
||||||
|
Name "${APP_NAME}"
|
||||||
|
Caption "${APP_NAME}"
|
||||||
|
OutFile "${INSTALLER_NAME}"
|
||||||
|
BrandingText "${APP_NAME}"
|
||||||
|
InstallDir "$PROGRAMFILES64\${APP_NAME}"
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
!include "MUI2.nsh"
|
||||||
|
!include fileassoc.nsh
|
||||||
|
|
||||||
|
!define MUI_ABORTWARNING
|
||||||
|
!define MUI_UNABORTWARNING
|
||||||
|
|
||||||
|
!define MUI_ICON "{{ cura_icon }}"
|
||||||
|
|
||||||
|
!define MUI_WELCOMEFINISHPAGE_BITMAP "{{ cura_banner_img }}"
|
||||||
|
!define MUI_UNWELCOMEFINISHPAGE_BITMAP "{{ cura_banner_img }}"
|
||||||
|
|
||||||
|
!insertmacro MUI_PAGE_WELCOME
|
||||||
|
|
||||||
|
!ifdef LICENSE_TXT
|
||||||
|
!insertmacro MUI_PAGE_LICENSE "${LICENSE_TXT}"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!insertmacro MUI_PAGE_DIRECTORY
|
||||||
|
|
||||||
|
!ifdef REG_START_MENU
|
||||||
|
!define MUI_STARTMENUPAGE_NODISABLE
|
||||||
|
!define MUI_STARTMENUPAGE_DEFAULTFOLDER "Ultimaker Cura"
|
||||||
|
!define MUI_STARTMENUPAGE_REGISTRY_ROOT "${REG_ROOT}"
|
||||||
|
!define MUI_STARTMENUPAGE_REGISTRY_KEY "${UNINSTALL_PATH}"
|
||||||
|
!define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "${REG_START_MENU}"
|
||||||
|
!insertmacro MUI_PAGE_STARTMENU Application $SM_Folder
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!insertmacro MUI_PAGE_INSTFILES
|
||||||
|
|
||||||
|
# Set up explorer to run Cura instead of directly, so it's not executed elevated (with all negative consequences that brings for an unelevated user).
|
||||||
|
!define MUI_FINISHPAGE_RUN "$WINDIR\explorer.exe"
|
||||||
|
!define MUI_FINISHPAGE_RUN_PARAMETERS "$INSTDIR\${MAIN_APP_EXE}"
|
||||||
|
!insertmacro MUI_PAGE_FINISH
|
||||||
|
|
||||||
|
!insertmacro MUI_UNPAGE_CONFIRM
|
||||||
|
|
||||||
|
!insertmacro MUI_UNPAGE_INSTFILES
|
||||||
|
|
||||||
|
!insertmacro MUI_UNPAGE_FINISH
|
||||||
|
|
||||||
|
!insertmacro MUI_LANGUAGE "English"
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
Section -MainProgram
|
||||||
|
${INSTALL_TYPE}
|
||||||
|
SetOverwrite ifnewer
|
||||||
|
{% for out_path, files in mapped_out_paths.items() %}SetOutPath "{{ out_path }}"{% for file in files %}
|
||||||
|
File "{{ file[0] }}"{% endfor %}
|
||||||
|
{% endfor %}SectionEnd
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
Section -Extension_Reg
|
||||||
|
!insertmacro APP_ASSOCIATE "stl" "Cura.model" "Standard Tessellation Language (STL) files" "$INSTDIR\${MAIN_APP_EXE},0" "Open with {{ app_name }}" "$INSTDIR\${MAIN_APP_EXE} $\"%1$\""
|
||||||
|
!insertmacro APP_ASSOCIATE "3mf" "Cura.project" "3D Manufacturing Format (3MF) files" "$INSTDIR\${MAIN_APP_EXE},0" "Open with {{ app_name }}" "$INSTDIR\${MAIN_APP_EXE} $\"%1$\""
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
Section -Icons_Reg
|
||||||
|
SetOutPath "$INSTDIR"
|
||||||
|
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
!ifdef REG_START_MENU
|
||||||
|
!insertmacro MUI_STARTMENU_WRITE_BEGIN Application
|
||||||
|
CreateDirectory "$SMPROGRAMS\$SM_Folder"
|
||||||
|
CreateShortCut "$SMPROGRAMS\$SM_Folder\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}"
|
||||||
|
CreateShortCut "$SMPROGRAMS\$SM_Folder\Uninstall ${APP_NAME}.lnk" "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
!ifdef WEB_SITE
|
||||||
|
WriteIniStr "$INSTDIR\Ultimaker Cura website.url" "InternetShortcut" "URL" "${WEB_SITE}"
|
||||||
|
CreateShortCut "$SMPROGRAMS\$SM_Folder\Ultimaker Cura website.lnk" "$INSTDIR\Ultimaker Cura website.url"
|
||||||
|
!endif
|
||||||
|
!insertmacro MUI_STARTMENU_WRITE_END
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifndef REG_START_MENU
|
||||||
|
CreateDirectory "$SMPROGRAMS\{{ app_name }}"
|
||||||
|
CreateShortCut "$SMPROGRAMS\{{ app_name }}\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}"
|
||||||
|
CreateShortCut "$SMPROGRAMS\{{ app_name }}\Uninstall ${APP_NAME}.lnk" "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
!ifdef WEB_SITE
|
||||||
|
WriteIniStr "$INSTDIR\Ultimaker Cura website.url" "InternetShortcut" "URL" "${WEB_SITE}"
|
||||||
|
CreateShortCut "$SMPROGRAMS\{{ app_name }}\Ultimaker Cura website.lnk" "$INSTDIR\Ultimaker Cura website.url"
|
||||||
|
!endif
|
||||||
|
!endif
|
||||||
|
|
||||||
|
WriteRegStr ${REG_ROOT} "${REG_APP_PATH}" "" "$INSTDIR\${MAIN_APP_EXE}"
|
||||||
|
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayName" "${APP_NAME}"
|
||||||
|
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "UninstallString" "$INSTDIR\uninstall.exe"
|
||||||
|
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayIcon" "$INSTDIR\${MAIN_APP_EXE}"
|
||||||
|
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayVersion" "${VERSION}"
|
||||||
|
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "Publisher" "${COMP_NAME}"
|
||||||
|
|
||||||
|
!ifdef WEB_SITE
|
||||||
|
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "URLInfoAbout" "${WEB_SITE}"
|
||||||
|
!endif
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
Section Uninstall
|
||||||
|
${INSTALL_TYPE}{% for files in mapped_out_paths.values() %}{% for file in files %}
|
||||||
|
Delete "{{ file[1] }}"{% endfor %}{% endfor %}{% for rem_dir in rmdir_paths %}
|
||||||
|
RmDir "{{ rem_dir }}"{% endfor %}
|
||||||
|
|
||||||
|
# FIXME: dirty solution, but for some reason these directories aren't removed
|
||||||
|
RmDir "$INSTDIR\share\cura\resources\scripts"
|
||||||
|
RmDir "$INSTDIR\share\cura\resources"
|
||||||
|
RmDir "$INSTDIR\share\cura"
|
||||||
|
RmDir "$INSTDIR\share\uranium\resources\scripts"
|
||||||
|
RmDir "$INSTDIR\share\uranium\resources"
|
||||||
|
RmDir "$INSTDIR\share\uranium"
|
||||||
|
RmDir "$INSTDIR\share"
|
||||||
|
|
||||||
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
|
!ifdef WEB_SITE
|
||||||
|
Delete "$INSTDIR\${APP_NAME} website.url"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
RmDir "$INSTDIR"
|
||||||
|
|
||||||
|
!ifdef REG_START_MENU
|
||||||
|
!insertmacro MUI_STARTMENU_GETFOLDER "Application" $SM_Folder
|
||||||
|
Delete "$SMPROGRAMS\$SM_Folder\${APP_NAME}.lnk"
|
||||||
|
Delete "$SMPROGRAMS\$SM_Folder\Uninstall ${APP_NAME}.lnk"
|
||||||
|
!ifdef WEB_SITE
|
||||||
|
Delete "$SMPROGRAMS\$SM_Folder\Ultimaker Cura website.lnk"
|
||||||
|
!endif
|
||||||
|
RmDir "$SMPROGRAMS\$SM_Folder"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifndef REG_START_MENU
|
||||||
|
Delete "$SMPROGRAMS\{{ app_name }}\${APP_NAME}.lnk"
|
||||||
|
Delete "$SMPROGRAMS\{{ app_name }}\Uninstall ${APP_NAME}.lnk"
|
||||||
|
!ifdef WEB_SITE
|
||||||
|
Delete "$SMPROGRAMS\{{ app_name }}\Ultimaker Cura website.lnk"
|
||||||
|
!endif
|
||||||
|
RmDir "$SMPROGRAMS\{{ app_name }}"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!insertmacro APP_UNASSOCIATE "stl" "Cura.model"
|
||||||
|
!insertmacro APP_UNASSOCIATE "3mf" "Cura.project"
|
||||||
|
|
||||||
|
DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
|
||||||
|
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
######################################################################
|
80
packaging/NSIS/create_windows_installer.py
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
# Copyright (c) 2022 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
import os
|
||||||
|
import argparse # Command line arguments parsing and help.
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from jinja2 import Template
|
||||||
|
|
||||||
|
|
||||||
|
def generate_nsi(source_path: str, dist_path: str, filename: str):
|
||||||
|
dist_loc = Path(os.getcwd(), dist_path)
|
||||||
|
source_loc = Path(os.getcwd(), source_path)
|
||||||
|
instdir = Path("$INSTDIR")
|
||||||
|
dist_paths = [p.relative_to(dist_loc.joinpath("Ultimaker-Cura")) for p in sorted(dist_loc.joinpath("Ultimaker-Cura").rglob("*")) if p.is_file()]
|
||||||
|
mapped_out_paths = {}
|
||||||
|
for dist_path in dist_paths:
|
||||||
|
if "__pycache__" not in dist_path.parts:
|
||||||
|
out_path = instdir.joinpath(dist_path).parent
|
||||||
|
if out_path not in mapped_out_paths:
|
||||||
|
mapped_out_paths[out_path] = [(dist_loc.joinpath("Ultimaker-Cura", dist_path), instdir.joinpath(dist_path))]
|
||||||
|
else:
|
||||||
|
mapped_out_paths[out_path].append((dist_loc.joinpath("Ultimaker-Cura", dist_path), instdir.joinpath(dist_path)))
|
||||||
|
|
||||||
|
rmdir_paths = set()
|
||||||
|
for rmdir_f in mapped_out_paths.values():
|
||||||
|
for _, rmdir_p in rmdir_f:
|
||||||
|
for rmdir in rmdir_p.parents:
|
||||||
|
rmdir_paths.add(rmdir)
|
||||||
|
|
||||||
|
rmdir_paths = sorted(list(rmdir_paths), reverse = True)[:-2] # Removes the `.` and `..` from the list
|
||||||
|
|
||||||
|
jinja_template_path = Path(source_loc.joinpath("packaging", "NSIS", "Ultimaker-Cura.nsi.jinja"))
|
||||||
|
with open(jinja_template_path, "r") as f:
|
||||||
|
template = Template(f.read())
|
||||||
|
|
||||||
|
|
||||||
|
nsis_content = template.render(
|
||||||
|
app_name = f"Ultimaker Cura {os.getenv('CURA_VERSION_FULL')}",
|
||||||
|
main_app = "Ultimaker-Cura.exe",
|
||||||
|
version = os.getenv('CURA_VERSION_FULL'),
|
||||||
|
version_major = os.environ.get("CURA_VERSION_MAJOR"),
|
||||||
|
version_minor = os.environ.get("CURA_VERSION_MINOR"),
|
||||||
|
version_patch = os.environ.get("CURA_VERSION_PATCH"),
|
||||||
|
company = "Ultimaker B.V.",
|
||||||
|
web_site = "https://ultimaker.com",
|
||||||
|
year = datetime.now().year,
|
||||||
|
cura_license_file = str(source_loc.joinpath("packaging", "cura_license.txt")),
|
||||||
|
compression_method = "LZMA", # ZLIB, BZIP2 or LZMA
|
||||||
|
cura_banner_img = str(source_loc.joinpath("packaging", "NSIS", "cura_banner_nsis.bmp")),
|
||||||
|
cura_icon = str(source_loc.joinpath("packaging", "icons", "Cura.ico")),
|
||||||
|
mapped_out_paths = mapped_out_paths,
|
||||||
|
rmdir_paths = rmdir_paths,
|
||||||
|
destination = filename
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(dist_loc.joinpath("Ultimaker-Cura.nsi"), "w") as f:
|
||||||
|
f.write(nsis_content)
|
||||||
|
|
||||||
|
shutil.copy(source_loc.joinpath("packaging", "NSIS", "fileassoc.nsh"), dist_loc.joinpath("fileassoc.nsh"))
|
||||||
|
|
||||||
|
|
||||||
|
def build(dist_path: str):
|
||||||
|
dist_loc = Path(os.getcwd(), dist_path)
|
||||||
|
command = ["makensis", "/V2", "/P4", str(dist_loc.joinpath("Ultimaker-Cura.nsi"))]
|
||||||
|
subprocess.run(command)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description = "Create Windows exe installer of Cura.")
|
||||||
|
parser.add_argument("source_path", type=str, help="Path to Conan install Cura folder.")
|
||||||
|
parser.add_argument("dist_path", type=str, help="Path to Pyinstaller dist folder")
|
||||||
|
parser.add_argument("filename", type = str, help = "Filename of the exe (e.g. 'Ultimaker-Cura-5.1.0-beta-Windows-X64.exe')")
|
||||||
|
args = parser.parse_args()
|
||||||
|
generate_nsi(args.source_path, args.dist_path, args.filename)
|
||||||
|
build(args.dist_path)
|
BIN
packaging/NSIS/cura_banner_nsis.bmp
Normal file
After Width: | Height: | Size: 201 KiB |
134
packaging/NSIS/fileassoc.nsh
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
; fileassoc.nsh
|
||||||
|
; File association helper macros
|
||||||
|
; Written by Saivert
|
||||||
|
;
|
||||||
|
; Improved by Nikku<https://github.com/nikku>.
|
||||||
|
;
|
||||||
|
; Features automatic backup system and UPDATEFILEASSOC macro for
|
||||||
|
; shell change notification.
|
||||||
|
;
|
||||||
|
; |> How to use <|
|
||||||
|
; To associate a file with an application so you can double-click it in explorer, use
|
||||||
|
; the APP_ASSOCIATE macro like this:
|
||||||
|
;
|
||||||
|
; Example:
|
||||||
|
; !insertmacro APP_ASSOCIATE "txt" "myapp.textfile" "Description of txt files" \
|
||||||
|
; "$INSTDIR\myapp.exe,0" "Open with myapp" "$INSTDIR\myapp.exe $\"%1$\""
|
||||||
|
;
|
||||||
|
; Never insert the APP_ASSOCIATE macro multiple times, it is only ment
|
||||||
|
; to associate an application with a single file and using the
|
||||||
|
; the "open" verb as default. To add more verbs (actions) to a file
|
||||||
|
; use the APP_ASSOCIATE_ADDVERB macro.
|
||||||
|
;
|
||||||
|
; Example:
|
||||||
|
; !insertmacro APP_ASSOCIATE_ADDVERB "myapp.textfile" "edit" "Edit with myapp" \
|
||||||
|
; "$INSTDIR\myapp.exe /edit $\"%1$\""
|
||||||
|
;
|
||||||
|
; To have access to more options when registering the file association use the
|
||||||
|
; APP_ASSOCIATE_EX macro. Here you can specify the verb and what verb is to be the
|
||||||
|
; standard action (default verb).
|
||||||
|
;
|
||||||
|
; Note, that this script takes into account user versus global installs.
|
||||||
|
; To properly work you must initialize the SHELL_CONTEXT variable via SetShellVarContext.
|
||||||
|
;
|
||||||
|
; And finally: To remove the association from the registry use the APP_UNASSOCIATE
|
||||||
|
; macro. Here is another example just to wrap it up:
|
||||||
|
; !insertmacro APP_UNASSOCIATE "txt" "myapp.textfile"
|
||||||
|
;
|
||||||
|
; |> Note <|
|
||||||
|
; When defining your file class string always use the short form of your application title
|
||||||
|
; then a period (dot) and the type of file. This keeps the file class sort of unique.
|
||||||
|
; Examples:
|
||||||
|
; Winamp.Playlist
|
||||||
|
; NSIS.Script
|
||||||
|
; Photoshop.JPEGFile
|
||||||
|
;
|
||||||
|
; |> Tech info <|
|
||||||
|
; The registry key layout for a global file association is:
|
||||||
|
;
|
||||||
|
; HKEY_LOCAL_MACHINE\Software\Classes
|
||||||
|
; <".ext"> = <applicationID>
|
||||||
|
; <applicationID> = <"description">
|
||||||
|
; shell
|
||||||
|
; <verb> = <"menu-item text">
|
||||||
|
; command = <"command string">
|
||||||
|
;
|
||||||
|
;
|
||||||
|
; The registry key layout for a per-user file association is:
|
||||||
|
;
|
||||||
|
; HKEY_CURRENT_USER\Software\Classes
|
||||||
|
; <".ext"> = <applicationID>
|
||||||
|
; <applicationID> = <"description">
|
||||||
|
; shell
|
||||||
|
; <verb> = <"menu-item text">
|
||||||
|
; command = <"command string">
|
||||||
|
;
|
||||||
|
|
||||||
|
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
|
||||||
|
; Backup the previously associated file class
|
||||||
|
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
|
||||||
|
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
|
||||||
|
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro APP_ASSOCIATE_EX EXT FILECLASS DESCRIPTION ICON VERB DEFAULTVERB SHELLNEW COMMANDTEXT COMMAND
|
||||||
|
; Backup the previously associated file class
|
||||||
|
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
|
||||||
|
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
|
||||||
|
StrCmp "${SHELLNEW}" "0" +2
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}\ShellNew" "NullFile" ""
|
||||||
|
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" `${DEFAULTVERB}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}" "" `${COMMANDTEXT}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}\command" "" `${COMMAND}`
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro APP_ASSOCIATE_ADDVERB FILECLASS VERB COMMANDTEXT COMMAND
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}" "" `${COMMANDTEXT}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}\command" "" `${COMMAND}`
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro APP_ASSOCIATE_REMOVEVERB FILECLASS VERB
|
||||||
|
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}\shell\${VERB}`
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
|
||||||
|
!macro APP_UNASSOCIATE EXT FILECLASS
|
||||||
|
; Backup the previously associated file class
|
||||||
|
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
|
||||||
|
|
||||||
|
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro APP_ASSOCIATE_GETFILECLASS OUTPUT EXT
|
||||||
|
ReadRegStr ${OUTPUT} SHELL_CONTEXT "Software\Classes\.${EXT}" ""
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
|
||||||
|
; !defines for use with SHChangeNotify
|
||||||
|
!ifdef SHCNE_ASSOCCHANGED
|
||||||
|
!undef SHCNE_ASSOCCHANGED
|
||||||
|
!endif
|
||||||
|
!define SHCNE_ASSOCCHANGED 0x08000000
|
||||||
|
!ifdef SHCNF_FLUSH
|
||||||
|
!undef SHCNF_FLUSH
|
||||||
|
!endif
|
||||||
|
!define SHCNF_FLUSH 0x1000
|
||||||
|
|
||||||
|
!macro UPDATEFILEASSOC
|
||||||
|
; Using the system.dll plugin to call the SHChangeNotify Win32 API function so we
|
||||||
|
; can update the shell.
|
||||||
|
System::Call "shell32::SHChangeNotify(i,i,i,i) (${SHCNE_ASSOCCHANGED}, ${SHCNF_FLUSH}, 0, 0)"
|
||||||
|
!macroend
|
165
packaging/cura_license.txt
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
|
||||||
|
This version of the GNU Lesser General Public License incorporates
|
||||||
|
the terms and conditions of version 3 of the GNU General Public
|
||||||
|
License, supplemented by the additional permissions listed below.
|
||||||
|
|
||||||
|
0. Additional Definitions.
|
||||||
|
|
||||||
|
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||||
|
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||||
|
General Public License.
|
||||||
|
|
||||||
|
"The Library" refers to a covered work governed by this License,
|
||||||
|
other than an Application or a Combined Work as defined below.
|
||||||
|
|
||||||
|
An "Application" is any work that makes use of an interface provided
|
||||||
|
by the Library, but which is not otherwise based on the Library.
|
||||||
|
Defining a subclass of a class defined by the Library is deemed a mode
|
||||||
|
of using an interface provided by the Library.
|
||||||
|
|
||||||
|
A "Combined Work" is a work produced by combining or linking an
|
||||||
|
Application with the Library. The particular version of the Library
|
||||||
|
with which the Combined Work was made is also called the "Linked
|
||||||
|
Version".
|
||||||
|
|
||||||
|
The "Minimal Corresponding Source" for a Combined Work means the
|
||||||
|
Corresponding Source for the Combined Work, excluding any source code
|
||||||
|
for portions of the Combined Work that, considered in isolation, are
|
||||||
|
based on the Application, and not on the Linked Version.
|
||||||
|
|
||||||
|
The "Corresponding Application Code" for a Combined Work means the
|
||||||
|
object code and/or source code for the Application, including any data
|
||||||
|
and utility programs needed for reproducing the Combined Work from the
|
||||||
|
Application, but excluding the System Libraries of the Combined Work.
|
||||||
|
|
||||||
|
1. Exception to Section 3 of the GNU GPL.
|
||||||
|
|
||||||
|
You may convey a covered work under sections 3 and 4 of this License
|
||||||
|
without being bound by section 3 of the GNU GPL.
|
||||||
|
|
||||||
|
2. Conveying Modified Versions.
|
||||||
|
|
||||||
|
If you modify a copy of the Library, and, in your modifications, a
|
||||||
|
facility refers to a function or data to be supplied by an Application
|
||||||
|
that uses the facility (other than as an argument passed when the
|
||||||
|
facility is invoked), then you may convey a copy of the modified
|
||||||
|
version:
|
||||||
|
|
||||||
|
a) under this License, provided that you make a good faith effort to
|
||||||
|
ensure that, in the event an Application does not supply the
|
||||||
|
function or data, the facility still operates, and performs
|
||||||
|
whatever part of its purpose remains meaningful, or
|
||||||
|
|
||||||
|
b) under the GNU GPL, with none of the additional permissions of
|
||||||
|
this License applicable to that copy.
|
||||||
|
|
||||||
|
3. Object Code Incorporating Material from Library Header Files.
|
||||||
|
|
||||||
|
The object code form of an Application may incorporate material from
|
||||||
|
a header file that is part of the Library. You may convey such object
|
||||||
|
code under terms of your choice, provided that, if the incorporated
|
||||||
|
material is not limited to numerical parameters, data structure
|
||||||
|
layouts and accessors, or small macros, inline functions and templates
|
||||||
|
(ten or fewer lines in length), you do both of the following:
|
||||||
|
|
||||||
|
a) Give prominent notice with each copy of the object code that the
|
||||||
|
Library is used in it and that the Library and its use are
|
||||||
|
covered by this License.
|
||||||
|
|
||||||
|
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||||
|
document.
|
||||||
|
|
||||||
|
4. Combined Works.
|
||||||
|
|
||||||
|
You may convey a Combined Work under terms of your choice that,
|
||||||
|
taken together, effectively do not restrict modification of the
|
||||||
|
portions of the Library contained in the Combined Work and reverse
|
||||||
|
engineering for debugging such modifications, if you also do each of
|
||||||
|
the following:
|
||||||
|
|
||||||
|
a) Give prominent notice with each copy of the Combined Work that
|
||||||
|
the Library is used in it and that the Library and its use are
|
||||||
|
covered by this License.
|
||||||
|
|
||||||
|
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||||
|
document.
|
||||||
|
|
||||||
|
c) For a Combined Work that displays copyright notices during
|
||||||
|
execution, include the copyright notice for the Library among
|
||||||
|
these notices, as well as a reference directing the user to the
|
||||||
|
copies of the GNU GPL and this license document.
|
||||||
|
|
||||||
|
d) Do one of the following:
|
||||||
|
|
||||||
|
0) Convey the Minimal Corresponding Source under the terms of this
|
||||||
|
License, and the Corresponding Application Code in a form
|
||||||
|
suitable for, and under terms that permit, the user to
|
||||||
|
recombine or relink the Application with a modified version of
|
||||||
|
the Linked Version to produce a modified Combined Work, in the
|
||||||
|
manner specified by section 6 of the GNU GPL for conveying
|
||||||
|
Corresponding Source.
|
||||||
|
|
||||||
|
1) Use a suitable shared library mechanism for linking with the
|
||||||
|
Library. A suitable mechanism is one that (a) uses at run time
|
||||||
|
a copy of the Library already present on the user's computer
|
||||||
|
system, and (b) will operate properly with a modified version
|
||||||
|
of the Library that is interface-compatible with the Linked
|
||||||
|
Version.
|
||||||
|
|
||||||
|
e) Provide Installation Information, but only if you would otherwise
|
||||||
|
be required to provide such information under section 6 of the
|
||||||
|
GNU GPL, and only to the extent that such information is
|
||||||
|
necessary to install and execute a modified version of the
|
||||||
|
Combined Work produced by recombining or relinking the
|
||||||
|
Application with a modified version of the Linked Version. (If
|
||||||
|
you use option 4d0, the Installation Information must accompany
|
||||||
|
the Minimal Corresponding Source and Corresponding Application
|
||||||
|
Code. If you use option 4d1, you must provide the Installation
|
||||||
|
Information in the manner specified by section 6 of the GNU GPL
|
||||||
|
for conveying Corresponding Source.)
|
||||||
|
|
||||||
|
5. Combined Libraries.
|
||||||
|
|
||||||
|
You may place library facilities that are a work based on the
|
||||||
|
Library side by side in a single library together with other library
|
||||||
|
facilities that are not Applications and are not covered by this
|
||||||
|
License, and convey such a combined library under terms of your
|
||||||
|
choice, if you do both of the following:
|
||||||
|
|
||||||
|
a) Accompany the combined library with a copy of the same work based
|
||||||
|
on the Library, uncombined with any other library facilities,
|
||||||
|
conveyed under the terms of this License.
|
||||||
|
|
||||||
|
b) Give prominent notice with the combined library that part of it
|
||||||
|
is a work based on the Library, and explaining where to find the
|
||||||
|
accompanying uncombined form of the same work.
|
||||||
|
|
||||||
|
6. Revised Versions of the GNU Lesser General Public License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the GNU Lesser General Public License from time to time. Such new
|
||||||
|
versions will be similar in spirit to the present version, but may
|
||||||
|
differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Library as you received it specifies that a certain numbered version
|
||||||
|
of the GNU Lesser General Public License "or any later version"
|
||||||
|
applies to it, you have the option of following the terms and
|
||||||
|
conditions either of that published version or of any later version
|
||||||
|
published by the Free Software Foundation. If the Library as you
|
||||||
|
received it does not specify a version number of the GNU Lesser
|
||||||
|
General Public License, you may choose any version of the GNU Lesser
|
||||||
|
General Public License ever published by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Library as you received it specifies that a proxy can decide
|
||||||
|
whether future versions of the GNU Lesser General Public License shall
|
||||||
|
apply, that proxy's public statement of acceptance of any version is
|
||||||
|
permanent authorization for you to choose that version for the
|
||||||
|
Library.
|
12
packaging/dmg/cura.entitlements
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.disable-executable-page-protection</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.disable-library-validation</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
BIN
packaging/dmg/cura_background_dmg.png
Normal file
After Width: | Height: | Size: 56 KiB |
67
packaging/dmg/dmg_sign_noterize.py
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import os
|
||||||
|
import argparse # Command line arguments parsing and help.
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
ULTIMAKER_CURA_DOMAIN = os.environ.get("ULTIMAKER_CURA_DOMAIN", "nl.ultimaker.cura")
|
||||||
|
|
||||||
|
|
||||||
|
def build_dmg(source_path: str, dist_path: str, filename: str) -> None:
|
||||||
|
create_dmg_executable = os.environ.get("CREATE_DMG_EXECUTABLE", "create-dmg")
|
||||||
|
|
||||||
|
arguments = [create_dmg_executable,
|
||||||
|
"--window-pos", "640", "360",
|
||||||
|
"--window-size", "690", "503",
|
||||||
|
"--app-drop-link", "520", "272",
|
||||||
|
"--volicon", f"{source_path}/packaging/icons/VolumeIcons_Cura.icns",
|
||||||
|
"--icon-size", "90",
|
||||||
|
"--icon", "Ultimaker-Cura.app", "169", "272",
|
||||||
|
"--eula", f"{source_path}/packaging/cura_license.txt",
|
||||||
|
"--background", f"{source_path}/packaging/dmg/cura_background_dmg.png",
|
||||||
|
f"{dist_path}/{filename}",
|
||||||
|
f"{dist_path}/Ultimaker-Cura.app"]
|
||||||
|
|
||||||
|
subprocess.run(arguments)
|
||||||
|
|
||||||
|
|
||||||
|
def sign(dist_path: str, filename: str) -> None:
|
||||||
|
codesign_executable = os.environ.get("CODESIGN", "codesign")
|
||||||
|
codesign_identity = os.environ.get("CODESIGN_IDENTITY")
|
||||||
|
|
||||||
|
arguments = [codesign_executable,
|
||||||
|
"-s", codesign_identity,
|
||||||
|
"--timestamp",
|
||||||
|
"-i", f"{ULTIMAKER_CURA_DOMAIN}.dmg", # TODO: check if this really should have the extra dmg. We seem to be doing this also in the old Rundeck scripts
|
||||||
|
f"{dist_path}/{filename}"]
|
||||||
|
|
||||||
|
subprocess.run(arguments)
|
||||||
|
|
||||||
|
|
||||||
|
def notarize(dist_path: str, filename: str) -> None:
|
||||||
|
notarize_user = os.environ.get("MAC_NOTARIZE_USER")
|
||||||
|
notarize_password = os.environ.get("MAC_NOTARIZE_PASS")
|
||||||
|
altool_executable = os.environ.get("ALTOOL_EXECUTABLE", "altool")
|
||||||
|
|
||||||
|
arguments = [
|
||||||
|
"xcrun", altool_executable,
|
||||||
|
"--notarize-app",
|
||||||
|
"--primary-bundle-id", ULTIMAKER_CURA_DOMAIN,
|
||||||
|
"--username", notarize_user,
|
||||||
|
"--password", notarize_password,
|
||||||
|
"--file", f"{dist_path}/{filename}"
|
||||||
|
]
|
||||||
|
|
||||||
|
subprocess.run(arguments)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description = "Create dmg of Cura.")
|
||||||
|
parser.add_argument("source_path", type=str, help="Path to Conan install Cura folder.")
|
||||||
|
parser.add_argument("dist_path", type=str, help="Path to Pyinstaller dist folder")
|
||||||
|
parser.add_argument("filename", type = str, help = "Filename of the dmg (e.g. 'Ultimaker-Cura-5.1.0-beta-Linux-X64.dmg')")
|
||||||
|
args = parser.parse_args()
|
||||||
|
build_dmg(args.source_path, args.dist_path, args.filename)
|
||||||
|
sign(args.dist_path, args.filename)
|
||||||
|
|
||||||
|
notarize_dmg = bool(os.environ.get("NOTARIZE_DMG", "TRUE"))
|
||||||
|
if notarize_dmg:
|
||||||
|
notarize(args.dist_path, args.filename)
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
BIN
packaging/icons/VolumeIcons_Cura.icns
Normal file
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
BIN
packaging/icons/cura-icon_128x128.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
packaging/icons/cura-icon_256x256.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
packaging/icons/cura-icon_64x64.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
packaging/icons/cura.icns
Normal file
|
@ -23,6 +23,7 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
|
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
|
||||||
from UM.Job import Job
|
from UM.Job import Job
|
||||||
from UM.Preferences import Preferences
|
from UM.Preferences import Preferences
|
||||||
|
from cura.CuraPackageManager import CuraPackageManager
|
||||||
|
|
||||||
from cura.Machines.ContainerTree import ContainerTree
|
from cura.Machines.ContainerTree import ContainerTree
|
||||||
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
||||||
|
@ -579,6 +580,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
is_printer_group = True
|
is_printer_group = True
|
||||||
machine_name = group_name
|
machine_name = group_name
|
||||||
|
|
||||||
|
# Getting missing required package ids
|
||||||
|
package_metadata = self._parse_packages_metadata(archive)
|
||||||
|
missing_package_metadata = self._filter_missing_package_metadata(package_metadata)
|
||||||
|
|
||||||
# Show the dialog, informing the user what is about to happen.
|
# Show the dialog, informing the user what is about to happen.
|
||||||
self._dialog.setMachineConflict(machine_conflict)
|
self._dialog.setMachineConflict(machine_conflict)
|
||||||
self._dialog.setIsPrinterGroup(is_printer_group)
|
self._dialog.setIsPrinterGroup(is_printer_group)
|
||||||
|
@ -599,6 +604,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
self._dialog.setExtruders(extruders)
|
self._dialog.setExtruders(extruders)
|
||||||
self._dialog.setVariantType(variant_type_name)
|
self._dialog.setVariantType(variant_type_name)
|
||||||
self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity)
|
self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity)
|
||||||
|
self._dialog.setMissingPackagesMetadata(missing_package_metadata)
|
||||||
self._dialog.show()
|
self._dialog.show()
|
||||||
|
|
||||||
# Block until the dialog is closed.
|
# Block until the dialog is closed.
|
||||||
|
@ -1243,3 +1249,29 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
metadata = data.iterfind("./um:metadata/um:name/um:label", {"um": "http://www.ultimaker.com/material"})
|
metadata = data.iterfind("./um:metadata/um:name/um:label", {"um": "http://www.ultimaker.com/material"})
|
||||||
for entry in metadata:
|
for entry in metadata:
|
||||||
return entry.text
|
return entry.text
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_packages_metadata(archive: zipfile.ZipFile) -> List[Dict[str, str]]:
|
||||||
|
try:
|
||||||
|
package_metadata = json.loads(archive.open("Cura/packages.json").read().decode("utf-8"))
|
||||||
|
return package_metadata["packages"]
|
||||||
|
except KeyError:
|
||||||
|
Logger.warning("No package metadata was found in .3mf file.")
|
||||||
|
except Exception:
|
||||||
|
Logger.error("Failed to load packages metadata from .3mf file.")
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _filter_missing_package_metadata(package_metadata: List[Dict[str, str]]) -> List[Dict[str, str]]:
|
||||||
|
"""Filters out installed packages from package_metadata"""
|
||||||
|
missing_packages = []
|
||||||
|
package_manager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager())
|
||||||
|
|
||||||
|
for package in package_metadata:
|
||||||
|
package_id = package["id"]
|
||||||
|
if not package_manager.isPackageInstalled(package_id):
|
||||||
|
missing_packages.append(package)
|
||||||
|
|
||||||
|
return missing_packages
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
# Copyright (c) 2020 Ultimaker B.V.
|
# Copyright (c) 2022 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from PyQt6.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication, QUrl
|
||||||
|
from PyQt6.QtGui import QDesktopServices
|
||||||
from typing import List, Optional, Dict, cast
|
from typing import List, Optional, Dict, cast
|
||||||
|
|
||||||
from PyQt6.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication
|
|
||||||
from UM.FlameProfiler import pyqtSlot
|
|
||||||
from UM.PluginRegistry import PluginRegistry
|
|
||||||
from UM.Application import Application
|
|
||||||
from UM.i18n import i18nCatalog
|
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
|
||||||
from cura.Settings.GlobalStack import GlobalStack
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Message import Message
|
||||||
|
from UM.PluginRegistry import PluginRegistry
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
|
||||||
from .UpdatableMachinesModel import UpdatableMachinesModel
|
from .UpdatableMachinesModel import UpdatableMachinesModel
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
@ -23,7 +28,7 @@ i18n_catalog = i18nCatalog("cura")
|
||||||
class WorkspaceDialog(QObject):
|
class WorkspaceDialog(QObject):
|
||||||
showDialogSignal = pyqtSignal()
|
showDialogSignal = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._component = None
|
self._component = None
|
||||||
self._context = None
|
self._context = None
|
||||||
|
@ -59,6 +64,9 @@ class WorkspaceDialog(QObject):
|
||||||
self._objects_on_plate = False
|
self._objects_on_plate = False
|
||||||
self._is_printer_group = False
|
self._is_printer_group = False
|
||||||
self._updatable_machines_model = UpdatableMachinesModel(self)
|
self._updatable_machines_model = UpdatableMachinesModel(self)
|
||||||
|
self._missing_package_metadata: List[Dict[str, str]] = []
|
||||||
|
self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry()
|
||||||
|
self._install_missing_package_dialog: Optional[QObject] = None
|
||||||
|
|
||||||
machineConflictChanged = pyqtSignal()
|
machineConflictChanged = pyqtSignal()
|
||||||
qualityChangesConflictChanged = pyqtSignal()
|
qualityChangesConflictChanged = pyqtSignal()
|
||||||
|
@ -79,6 +87,7 @@ class WorkspaceDialog(QObject):
|
||||||
variantTypeChanged = pyqtSignal()
|
variantTypeChanged = pyqtSignal()
|
||||||
extrudersChanged = pyqtSignal()
|
extrudersChanged = pyqtSignal()
|
||||||
isPrinterGroupChanged = pyqtSignal()
|
isPrinterGroupChanged = pyqtSignal()
|
||||||
|
missingPackagesChanged = pyqtSignal()
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = isPrinterGroupChanged)
|
@pyqtProperty(bool, notify = isPrinterGroupChanged)
|
||||||
def isPrinterGroup(self) -> bool:
|
def isPrinterGroup(self) -> bool:
|
||||||
|
@ -274,6 +283,21 @@ class WorkspaceDialog(QObject):
|
||||||
self._has_quality_changes_conflict = quality_changes_conflict
|
self._has_quality_changes_conflict = quality_changes_conflict
|
||||||
self.qualityChangesConflictChanged.emit()
|
self.qualityChangesConflictChanged.emit()
|
||||||
|
|
||||||
|
def setMissingPackagesMetadata(self, missing_package_metadata: List[Dict[str, str]]) -> None:
|
||||||
|
self._missing_package_metadata = missing_package_metadata
|
||||||
|
self.missingPackagesChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantList", notify=missingPackagesChanged)
|
||||||
|
def missingPackages(self) -> List[Dict[str, str]]:
|
||||||
|
return self._missing_package_metadata
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def installMissingPackages(self) -> None:
|
||||||
|
marketplace_plugin = PluginRegistry.getInstance().getPluginObject("Marketplace")
|
||||||
|
if not marketplace_plugin:
|
||||||
|
Logger.warning("Could not show dialog to install missing plug-ins. Is Marketplace plug-in not available?")
|
||||||
|
marketplace_plugin.showInstallMissingPackageDialog(self._missing_package_metadata, self.showMissingMaterialsWarning) # type: ignore
|
||||||
|
|
||||||
def getResult(self) -> Dict[str, Optional[str]]:
|
def getResult(self) -> Dict[str, Optional[str]]:
|
||||||
if "machine" in self._result and self.updatableMachinesModel.count <= 1:
|
if "machine" in self._result and self.updatableMachinesModel.count <= 1:
|
||||||
self._result["machine"] = None
|
self._result["machine"] = None
|
||||||
|
@ -360,6 +384,41 @@ class WorkspaceDialog(QObject):
|
||||||
time.sleep(1 / 50)
|
time.sleep(1 / 50)
|
||||||
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def showMissingMaterialsWarning(self) -> None:
|
||||||
|
result_message = Message(
|
||||||
|
i18n_catalog.i18nc("@info:status", "The material used in this project relies on some material definitions not available in Cura, this might produce undesirable print results. We highly recommend installing the full material package from the Marketplace."),
|
||||||
|
lifetime=0,
|
||||||
|
title=i18n_catalog.i18nc("@info:title", "Material profiles not installed"),
|
||||||
|
message_type=Message.MessageType.WARNING
|
||||||
|
)
|
||||||
|
result_message.addAction(
|
||||||
|
"learn_more",
|
||||||
|
name=i18n_catalog.i18nc("@action:button", "Learn more"),
|
||||||
|
icon="",
|
||||||
|
description="Learn more about project materials.",
|
||||||
|
button_align=Message.ActionButtonAlignment.ALIGN_LEFT,
|
||||||
|
button_style=Message.ActionButtonStyle.LINK
|
||||||
|
)
|
||||||
|
result_message.addAction(
|
||||||
|
"install_materials",
|
||||||
|
name=i18n_catalog.i18nc("@action:button", "Install Materials"),
|
||||||
|
icon="",
|
||||||
|
description="Install missing materials from project file.",
|
||||||
|
button_align=Message.ActionButtonAlignment.ALIGN_RIGHT,
|
||||||
|
button_style=Message.ActionButtonStyle.DEFAULT
|
||||||
|
)
|
||||||
|
result_message.actionTriggered.connect(self._onMessageActionTriggered)
|
||||||
|
result_message.show()
|
||||||
|
|
||||||
|
def _onMessageActionTriggered(self, message: Message, sync_message_action: str) -> None:
|
||||||
|
if sync_message_action == "install_materials":
|
||||||
|
self.installMissingPackages()
|
||||||
|
message.hide()
|
||||||
|
elif sync_message_action == "learn_more":
|
||||||
|
QDesktopServices.openUrl(QUrl("https://support.ultimaker.com/hc/en-us/articles/360011968360-Using-the-Ultimaker-Marketplace"))
|
||||||
|
|
||||||
|
|
||||||
def __show(self) -> None:
|
def __show(self) -> None:
|
||||||
if self._view is None:
|
if self._view is None:
|
||||||
self._createViewFromQML()
|
self._createViewFromQML()
|
||||||
|
|
|
@ -17,7 +17,8 @@ UM.Dialog
|
||||||
minimumWidth: UM.Theme.getSize("popup_dialog").width
|
minimumWidth: UM.Theme.getSize("popup_dialog").width
|
||||||
minimumHeight: UM.Theme.getSize("popup_dialog").height
|
minimumHeight: UM.Theme.getSize("popup_dialog").height
|
||||||
width: minimumWidth
|
width: minimumWidth
|
||||||
|
backgroundColor: UM.Theme.getColor("main_background")
|
||||||
|
margin: UM.Theme.getSize("default_margin").width
|
||||||
property int comboboxHeight: UM.Theme.getSize("default_margin").height
|
property int comboboxHeight: UM.Theme.getSize("default_margin").height
|
||||||
|
|
||||||
onClosing: manager.notifyClosed()
|
onClosing: manager.notifyClosed()
|
||||||
|
@ -31,337 +32,220 @@ UM.Dialog
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item
|
Flickable
|
||||||
{
|
{
|
||||||
id: dialogSummaryItem
|
clip: true
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: childrenRect.height
|
height: parent.height
|
||||||
anchors.margins: 10 * screenScaleFactor
|
contentHeight: dialogSummaryItem.height
|
||||||
|
ScrollBar.vertical: UM.ScrollBar { id: verticalScrollBar }
|
||||||
|
|
||||||
UM.I18nCatalog
|
Item
|
||||||
{
|
{
|
||||||
id: catalog
|
id: dialogSummaryItem
|
||||||
name: "cura"
|
width: verticalScrollBar.visible ? parent.width - verticalScrollBar.width - UM.Theme.getSize("default_margin").width : parent.width
|
||||||
}
|
|
||||||
|
|
||||||
ListModel
|
|
||||||
{
|
|
||||||
id: resolveStrategiesModel
|
|
||||||
// Instead of directly adding the list elements, we add them afterwards.
|
|
||||||
// This is because it's impossible to use setting function results to be bound to listElement properties directly.
|
|
||||||
// See http://stackoverflow.com/questions/7659442/listelement-fields-as-properties
|
|
||||||
Component.onCompleted:
|
|
||||||
{
|
|
||||||
append({"key": "override", "label": catalog.i18nc("@action:ComboBox Update/override existing profile", "Update existing")});
|
|
||||||
append({"key": "new", "label": catalog.i18nc("@action:ComboBox Save settings in a new profile", "Create new")});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column
|
|
||||||
{
|
|
||||||
width: parent.width
|
|
||||||
height: childrenRect.height
|
height: childrenRect.height
|
||||||
spacing: UM.Theme.getSize("default_margin").height
|
anchors.margins: 10 * screenScaleFactor
|
||||||
|
|
||||||
|
UM.I18nCatalog
|
||||||
|
{
|
||||||
|
id: catalog
|
||||||
|
name: "cura"
|
||||||
|
}
|
||||||
|
|
||||||
|
ListModel
|
||||||
|
{
|
||||||
|
id: resolveStrategiesModel
|
||||||
|
// Instead of directly adding the list elements, we add them afterwards.
|
||||||
|
// This is because it's impossible to use setting function results to be bound to listElement properties directly.
|
||||||
|
// See http://stackoverflow.com/questions/7659442/listelement-fields-as-properties
|
||||||
|
Component.onCompleted:
|
||||||
|
{
|
||||||
|
append({"key": "override", "label": catalog.i18nc("@action:ComboBox Update/override existing profile", "Update existing")});
|
||||||
|
append({"key": "new", "label": catalog.i18nc("@action:ComboBox Save settings in a new profile", "Create new")});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column
|
Column
|
||||||
{
|
{
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: childrenRect.height
|
height: childrenRect.height
|
||||||
|
spacing: UM.Theme.getSize("default_margin").height
|
||||||
|
|
||||||
UM.Label
|
Column
|
||||||
{
|
{
|
||||||
id: titleLabel
|
|
||||||
text: catalog.i18nc("@action:title", "Summary - Cura Project")
|
|
||||||
font: UM.Theme.getFont("large")
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle
|
|
||||||
{
|
|
||||||
id: separator
|
|
||||||
color: UM.Theme.getColor("text")
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: UM.Theme.getSize("default_lining").height
|
height: childrenRect.height
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item
|
UM.Label
|
||||||
{
|
|
||||||
width: parent.width
|
|
||||||
height: childrenRect.height
|
|
||||||
|
|
||||||
UM.TooltipArea
|
|
||||||
{
|
|
||||||
id: machineResolveStrategyTooltip
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.right: parent.right
|
|
||||||
width: (parent.width / 3) | 0
|
|
||||||
height: visible ? comboboxHeight : 0
|
|
||||||
visible: base.visible && machineResolveComboBox.model.count > 1
|
|
||||||
text: catalog.i18nc("@info:tooltip", "How should the conflict in the machine be resolved?")
|
|
||||||
Cura.ComboBox
|
|
||||||
{
|
{
|
||||||
id: machineResolveComboBox
|
id: titleLabel
|
||||||
model: manager.updatableMachinesModel
|
text: catalog.i18nc("@action:title", "Summary - Cura Project")
|
||||||
visible: machineResolveStrategyTooltip.visible
|
font: UM.Theme.getFont("large")
|
||||||
textRole: "displayName"
|
}
|
||||||
|
|
||||||
|
Rectangle
|
||||||
|
{
|
||||||
|
id: separator
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: UM.Theme.getSize("button").height
|
height: UM.Theme.getSize("default_lining").height
|
||||||
onCurrentIndexChanged:
|
}
|
||||||
{
|
}
|
||||||
if (model.getItem(currentIndex).id == "new"
|
|
||||||
&& model.getItem(currentIndex).type == "default_option")
|
|
||||||
{
|
|
||||||
manager.setResolveStrategy("machine", "new")
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
manager.setResolveStrategy("machine", "override")
|
|
||||||
manager.setMachineToOverride(model.getItem(currentIndex).id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged:
|
Item
|
||||||
{
|
{
|
||||||
if (!visible) {return}
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
|
||||||
currentIndex = 0
|
UM.TooltipArea
|
||||||
// If the project printer exists in Cura, set it as the default dropdown menu option.
|
{
|
||||||
// No need to check object 0, which is the "Create new" option
|
id: machineResolveStrategyTooltip
|
||||||
for (var i = 1; i < model.count; i++)
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
height: visible ? comboboxHeight : 0
|
||||||
|
visible: base.visible && machineResolveComboBox.model.count > 1
|
||||||
|
text: catalog.i18nc("@info:tooltip", "How should the conflict in the machine be resolved?")
|
||||||
|
Cura.ComboBox
|
||||||
|
{
|
||||||
|
id: machineResolveComboBox
|
||||||
|
model: manager.updatableMachinesModel
|
||||||
|
visible: machineResolveStrategyTooltip.visible
|
||||||
|
textRole: "displayName"
|
||||||
|
width: parent.width
|
||||||
|
height: UM.Theme.getSize("button").height
|
||||||
|
onCurrentIndexChanged:
|
||||||
{
|
{
|
||||||
if (model.getItem(i).name == manager.machineName)
|
if (model.getItem(currentIndex).id == "new"
|
||||||
|
&& model.getItem(currentIndex).type == "default_option")
|
||||||
{
|
{
|
||||||
currentIndex = i
|
manager.setResolveStrategy("machine", "new")
|
||||||
break
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
manager.setResolveStrategy("machine", "override")
|
||||||
|
manager.setMachineToOverride(model.getItem(currentIndex).id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The project printer does not exist in Cura. If there is at least one printer of the same
|
|
||||||
// type, select the first one, else set the index to "Create new"
|
onVisibleChanged:
|
||||||
if (currentIndex == 0 && model.count > 1)
|
|
||||||
{
|
{
|
||||||
currentIndex = 1
|
if (!visible) {return}
|
||||||
|
|
||||||
|
currentIndex = 0
|
||||||
|
// If the project printer exists in Cura, set it as the default dropdown menu option.
|
||||||
|
// No need to check object 0, which is the "Create new" option
|
||||||
|
for (var i = 1; i < model.count; i++)
|
||||||
|
{
|
||||||
|
if (model.getItem(i).name == manager.machineName)
|
||||||
|
{
|
||||||
|
currentIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The project printer does not exist in Cura. If there is at least one printer of the same
|
||||||
|
// type, select the first one, else set the index to "Create new"
|
||||||
|
if (currentIndex == 0 && model.count > 1)
|
||||||
|
{
|
||||||
|
currentIndex = 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Column
|
Column
|
||||||
{
|
|
||||||
width: parent.width
|
|
||||||
height: childrenRect.height
|
|
||||||
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
id: printer_settings_label
|
|
||||||
text: catalog.i18nc("@action:label", "Printer settings")
|
|
||||||
font: UM.Theme.getFont("default_bold")
|
|
||||||
}
|
|
||||||
|
|
||||||
Row
|
|
||||||
{
|
{
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: childrenRect.height
|
height: childrenRect.height
|
||||||
|
|
||||||
UM.Label
|
UM.Label
|
||||||
{
|
{
|
||||||
text: catalog.i18nc("@action:label", "Type")
|
id: printer_settings_label
|
||||||
width: (parent.width / 3) | 0
|
text: catalog.i18nc("@action:label", "Printer settings")
|
||||||
}
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: manager.machineType
|
|
||||||
width: (parent.width / 3) | 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row
|
|
||||||
{
|
|
||||||
width: parent.width
|
|
||||||
height: childrenRect.height
|
|
||||||
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@action:label", manager.isPrinterGroup ? "Printer Group" : "Printer Name")
|
|
||||||
width: (parent.width / 3) | 0
|
|
||||||
}
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: manager.machineName
|
|
||||||
width: (parent.width / 3) | 0
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item
|
|
||||||
{
|
|
||||||
width: parent.width
|
|
||||||
height: childrenRect.height
|
|
||||||
|
|
||||||
UM.TooltipArea
|
|
||||||
{
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
width: (parent.width / 3) | 0
|
|
||||||
height: visible ? comboboxHeight : 0
|
|
||||||
visible: manager.qualityChangesConflict
|
|
||||||
text: catalog.i18nc("@info:tooltip", "How should the conflict in the profile be resolved?")
|
|
||||||
Cura.ComboBox
|
|
||||||
{
|
|
||||||
model: resolveStrategiesModel
|
|
||||||
textRole: "label"
|
|
||||||
id: qualityChangesResolveComboBox
|
|
||||||
width: parent.width
|
|
||||||
height: UM.Theme.getSize("button").height
|
|
||||||
onActivated:
|
|
||||||
{
|
|
||||||
manager.setResolveStrategy("quality_changes", resolveStrategiesModel.get(index).key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column
|
|
||||||
{
|
|
||||||
width: parent.width
|
|
||||||
height: childrenRect.height
|
|
||||||
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@action:label", "Profile settings")
|
|
||||||
font: UM.Theme.getFont("default_bold")
|
|
||||||
}
|
|
||||||
|
|
||||||
Row
|
|
||||||
{
|
|
||||||
width: parent.width
|
|
||||||
height: childrenRect.height
|
|
||||||
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@action:label", "Name")
|
|
||||||
width: (parent.width / 3) | 0
|
|
||||||
}
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: manager.qualityName
|
|
||||||
width: (parent.width / 3) | 0
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row
|
|
||||||
{
|
|
||||||
width: parent.width
|
|
||||||
height: childrenRect.height
|
|
||||||
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@action:label", "Intent")
|
|
||||||
width: (parent.width / 3) | 0
|
|
||||||
}
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: manager.intentName
|
|
||||||
width: (parent.width / 3) | 0
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row
|
|
||||||
{
|
|
||||||
width: parent.width
|
|
||||||
height: childrenRect.height
|
|
||||||
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@action:label", "Not in profile")
|
|
||||||
visible: manager.numUserSettings != 0
|
|
||||||
width: (parent.width / 3) | 0
|
|
||||||
}
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: catalog.i18ncp("@action:label", "%1 override", "%1 overrides", manager.numUserSettings).arg(manager.numUserSettings)
|
|
||||||
visible: manager.numUserSettings != 0
|
|
||||||
width: (parent.width / 3) | 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row
|
|
||||||
{
|
|
||||||
width: parent.width
|
|
||||||
height: childrenRect.height
|
|
||||||
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@action:label", "Derivative from")
|
|
||||||
visible: manager.numSettingsOverridenByQualityChanges != 0
|
|
||||||
width: (parent.width / 3) | 0
|
|
||||||
}
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: catalog.i18ncp("@action:label", "%1, %2 override", "%1, %2 overrides", manager.numSettingsOverridenByQualityChanges).arg(manager.qualityType).arg(manager.numSettingsOverridenByQualityChanges)
|
|
||||||
width: (parent.width / 3) | 0
|
|
||||||
visible: manager.numSettingsOverridenByQualityChanges != 0
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item
|
|
||||||
{
|
|
||||||
width: parent.width
|
|
||||||
height: childrenRect.height
|
|
||||||
|
|
||||||
UM.TooltipArea
|
|
||||||
{
|
|
||||||
id: materialResolveTooltip
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
width: (parent.width / 3) | 0
|
|
||||||
height: visible ? comboboxHeight : 0
|
|
||||||
visible: manager.materialConflict
|
|
||||||
text: catalog.i18nc("@info:tooltip", "How should the conflict in the material be resolved?")
|
|
||||||
Cura.ComboBox
|
|
||||||
{
|
|
||||||
model: resolveStrategiesModel
|
|
||||||
textRole: "label"
|
|
||||||
id: materialResolveComboBox
|
|
||||||
width: parent.width
|
|
||||||
height: UM.Theme.getSize("button").height
|
|
||||||
onActivated:
|
|
||||||
{
|
|
||||||
manager.setResolveStrategy("material", resolveStrategiesModel.get(index).key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column
|
|
||||||
{
|
|
||||||
width: parent.width
|
|
||||||
height: childrenRect.height
|
|
||||||
Row
|
|
||||||
{
|
|
||||||
height: childrenRect.height
|
|
||||||
width: parent.width
|
|
||||||
spacing: UM.Theme.getSize("narrow_margin").width
|
|
||||||
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@action:label", "Material settings")
|
|
||||||
font: UM.Theme.getFont("default_bold")
|
font: UM.Theme.getFont("default_bold")
|
||||||
width: (parent.width / 3) | 0
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Repeater
|
Row
|
||||||
{
|
|
||||||
model: manager.materialLabels
|
|
||||||
delegate: Row
|
|
||||||
{
|
{
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: childrenRect.height
|
height: childrenRect.height
|
||||||
|
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "Type")
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
text: manager.machineType
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", manager.isPrinterGroup ? "Printer Group" : "Printer Name")
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
text: manager.machineName
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
|
||||||
|
UM.TooltipArea
|
||||||
|
{
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
height: visible ? comboboxHeight : 0
|
||||||
|
visible: manager.qualityChangesConflict
|
||||||
|
text: catalog.i18nc("@info:tooltip", "How should the conflict in the profile be resolved?")
|
||||||
|
Cura.ComboBox
|
||||||
|
{
|
||||||
|
model: resolveStrategiesModel
|
||||||
|
textRole: "label"
|
||||||
|
id: qualityChangesResolveComboBox
|
||||||
|
width: parent.width
|
||||||
|
height: UM.Theme.getSize("button").height
|
||||||
|
onActivated:
|
||||||
|
{
|
||||||
|
manager.setResolveStrategy("quality_changes", resolveStrategiesModel.get(index).key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "Profile settings")
|
||||||
|
font: UM.Theme.getFont("default_bold")
|
||||||
|
}
|
||||||
|
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
|
||||||
UM.Label
|
UM.Label
|
||||||
{
|
{
|
||||||
text: catalog.i18nc("@action:label", "Name")
|
text: catalog.i18nc("@action:label", "Name")
|
||||||
|
@ -369,77 +253,250 @@ UM.Dialog
|
||||||
}
|
}
|
||||||
UM.Label
|
UM.Label
|
||||||
{
|
{
|
||||||
text: modelData
|
text: manager.qualityName
|
||||||
width: (parent.width / 3) | 0
|
width: (parent.width / 3) | 0
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "Intent")
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
text: manager.intentName
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "Not in profile")
|
||||||
|
visible: manager.numUserSettings != 0
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
text: catalog.i18ncp("@action:label", "%1 override", "%1 overrides", manager.numUserSettings).arg(manager.numUserSettings)
|
||||||
|
visible: manager.numUserSettings != 0
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "Derivative from")
|
||||||
|
visible: manager.numSettingsOverridenByQualityChanges != 0
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
text: catalog.i18ncp("@action:label", "%1, %2 override", "%1, %2 overrides", manager.numSettingsOverridenByQualityChanges).arg(manager.qualityType).arg(manager.numSettingsOverridenByQualityChanges)
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
visible: manager.numSettingsOverridenByQualityChanges != 0
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Column
|
Item
|
||||||
{
|
|
||||||
width: parent.width
|
|
||||||
height: childrenRect.height
|
|
||||||
|
|
||||||
UM.Label
|
|
||||||
{
|
{
|
||||||
text: catalog.i18nc("@action:label", "Setting visibility")
|
width: parent.width
|
||||||
font: UM.Theme.getFont("default_bold")
|
height: childrenRect.height
|
||||||
|
|
||||||
|
UM.TooltipArea
|
||||||
|
{
|
||||||
|
id: materialResolveTooltip
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
height: visible ? comboboxHeight : 0
|
||||||
|
visible: manager.materialConflict
|
||||||
|
text: catalog.i18nc("@info:tooltip", "How should the conflict in the material be resolved?")
|
||||||
|
Cura.ComboBox
|
||||||
|
{
|
||||||
|
model: resolveStrategiesModel
|
||||||
|
textRole: "label"
|
||||||
|
id: materialResolveComboBox
|
||||||
|
width: parent.width
|
||||||
|
height: UM.Theme.getSize("button").height
|
||||||
|
onActivated:
|
||||||
|
{
|
||||||
|
manager.setResolveStrategy("material", resolveStrategiesModel.get(index).key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
height: childrenRect.height
|
||||||
|
width: parent.width
|
||||||
|
spacing: UM.Theme.getSize("narrow_margin").width
|
||||||
|
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "Material settings")
|
||||||
|
font: UM.Theme.getFont("default_bold")
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater
|
||||||
|
{
|
||||||
|
model: manager.materialLabels
|
||||||
|
delegate: Row
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "Name")
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
text: modelData
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Column
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "Setting visibility")
|
||||||
|
font: UM.Theme.getFont("default_bold")
|
||||||
|
}
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "Mode")
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
text: manager.activeMode
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
visible: manager.hasVisibleSettingsField
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "Visible settings:")
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "%1 out of %2" ).arg(manager.numVisibleSettings).arg(manager.totalNumberOfSettings)
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Row
|
Row
|
||||||
{
|
{
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: childrenRect.height
|
height: childrenRect.height
|
||||||
UM.Label
|
visible: manager.hasObjectsOnPlate
|
||||||
|
UM.ColorImage
|
||||||
{
|
{
|
||||||
text: catalog.i18nc("@action:label", "Mode")
|
width: warningLabel.height
|
||||||
width: (parent.width / 3) | 0
|
height: width
|
||||||
|
source: UM.Theme.getIcon("Information")
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
}
|
}
|
||||||
UM.Label
|
UM.Label
|
||||||
{
|
{
|
||||||
text: manager.activeMode
|
id: warningLabel
|
||||||
width: (parent.width / 3) | 0
|
text: catalog.i18nc("@action:warning", "Loading a project will clear all models on the build plate.")
|
||||||
}
|
|
||||||
}
|
|
||||||
Row
|
|
||||||
{
|
|
||||||
width: parent.width
|
|
||||||
height: childrenRect.height
|
|
||||||
visible: manager.hasVisibleSettingsField
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@action:label", "Visible settings:")
|
|
||||||
width: (parent.width / 3) | 0
|
|
||||||
}
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@action:label", "%1 out of %2" ).arg(manager.numVisibleSettings).arg(manager.totalNumberOfSettings)
|
|
||||||
width: (parent.width / 3) | 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Row
|
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
|
||||||
{
|
{
|
||||||
width: parent.width
|
id: warningRow
|
||||||
height: childrenRect.height
|
height: childrenRect.height
|
||||||
visible: manager.hasObjectsOnPlate
|
visible: warning
|
||||||
|
spacing: base.margin
|
||||||
UM.ColorImage
|
UM.ColorImage
|
||||||
{
|
{
|
||||||
width: warningLabel.height
|
width: UM.Theme.getSize("extruder_icon").width
|
||||||
height: width
|
height: UM.Theme.getSize("extruder_icon").height
|
||||||
source: UM.Theme.getIcon("Information")
|
source: UM.Theme.getIcon("Warning")
|
||||||
color: UM.Theme.getColor("text")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UM.Label
|
UM.Label
|
||||||
{
|
{
|
||||||
id: warningLabel
|
id: warningText
|
||||||
text: catalog.i18nc("@action:warning", "Loading a project will clear all models on the build plate.")
|
text: catalog.i18nc("@label", "The material used in this project is currently not installed in Cura.<br/>Install the material profile and reopen the project.")
|
||||||
wrapMode: Text.Wrap
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
sourceComponent: buttonRow
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -448,13 +505,30 @@ UM.Dialog
|
||||||
rightButtons: [
|
rightButtons: [
|
||||||
Cura.TertiaryButton
|
Cura.TertiaryButton
|
||||||
{
|
{
|
||||||
|
visible: !warning
|
||||||
text: catalog.i18nc("@action:button", "Cancel")
|
text: catalog.i18nc("@action:button", "Cancel")
|
||||||
onClicked: reject()
|
onClicked: reject()
|
||||||
},
|
},
|
||||||
Cura.PrimaryButton
|
Cura.PrimaryButton
|
||||||
{
|
{
|
||||||
|
visible: !warning
|
||||||
text: catalog.i18nc("@action:button", "Open")
|
text: catalog.i18nc("@action:button", "Open")
|
||||||
onClicked: accept()
|
onClicked: accept()
|
||||||
|
},
|
||||||
|
Cura.TertiaryButton
|
||||||
|
{
|
||||||
|
visible: warning
|
||||||
|
text: catalog.i18nc("@action:button", "Open project anyway")
|
||||||
|
onClicked: {
|
||||||
|
manager.showMissingMaterialsWarning();
|
||||||
|
accept();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Cura.PrimaryButton
|
||||||
|
{
|
||||||
|
visible: warning
|
||||||
|
text: catalog.i18nc("@action:button", "Install missing material")
|
||||||
|
onClicked: manager.installMissingPackages()
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,6 @@
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Provides support for reading 3MF files.",
|
"description": "Provides support for reading 3MF files.",
|
||||||
"api": 7,
|
"api": 8,
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,22 @@
|
||||||
# Copyright (c) 2015-2022 Ultimaker B.V.
|
# Copyright (c) 2015-2022 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from typing import Optional
|
import json
|
||||||
|
|
||||||
|
from typing import Optional, cast, List, Dict
|
||||||
|
|
||||||
from UM.Mesh.MeshWriter import MeshWriter
|
from UM.Mesh.MeshWriter import MeshWriter
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Math.Matrix import Matrix
|
from UM.Math.Matrix import Matrix
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
from UM.Message import Message
|
||||||
|
from UM.Resources import Resources
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
|
||||||
|
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
|
from cura.CuraPackageManager import CuraPackageManager
|
||||||
from cura.Utils.Threading import call_on_qt_thread
|
from cura.Utils.Threading import call_on_qt_thread
|
||||||
from cura.Snapshot import Snapshot
|
from cura.Snapshot import Snapshot
|
||||||
|
|
||||||
|
@ -34,6 +41,9 @@ import UM.Application
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
THUMBNAIL_PATH = "Metadata/thumbnail.png"
|
||||||
|
MODEL_PATH = "3D/3dmodel.model"
|
||||||
|
PACKAGE_METADATA_PATH = "Cura/packages.json"
|
||||||
|
|
||||||
class ThreeMFWriter(MeshWriter):
|
class ThreeMFWriter(MeshWriter):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -46,7 +56,7 @@ class ThreeMFWriter(MeshWriter):
|
||||||
}
|
}
|
||||||
|
|
||||||
self._unit_matrix_string = self._convertMatrixToString(Matrix())
|
self._unit_matrix_string = self._convertMatrixToString(Matrix())
|
||||||
self._archive = None # type: Optional[zipfile.ZipFile]
|
self._archive: Optional[zipfile.ZipFile] = None
|
||||||
self._store_archive = False
|
self._store_archive = False
|
||||||
|
|
||||||
def _convertMatrixToString(self, matrix):
|
def _convertMatrixToString(self, matrix):
|
||||||
|
@ -132,11 +142,11 @@ class ThreeMFWriter(MeshWriter):
|
||||||
def getArchive(self):
|
def getArchive(self):
|
||||||
return self._archive
|
return self._archive
|
||||||
|
|
||||||
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode):
|
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode) -> bool:
|
||||||
self._archive = None # Reset archive
|
self._archive = None # Reset archive
|
||||||
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
|
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
|
||||||
try:
|
try:
|
||||||
model_file = zipfile.ZipInfo("3D/3dmodel.model")
|
model_file = zipfile.ZipInfo(MODEL_PATH)
|
||||||
# Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo.
|
# Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo.
|
||||||
model_file.compress_type = zipfile.ZIP_DEFLATED
|
model_file.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
|
||||||
|
@ -151,7 +161,7 @@ class ThreeMFWriter(MeshWriter):
|
||||||
relations_file = zipfile.ZipInfo("_rels/.rels")
|
relations_file = zipfile.ZipInfo("_rels/.rels")
|
||||||
relations_file.compress_type = zipfile.ZIP_DEFLATED
|
relations_file.compress_type = zipfile.ZIP_DEFLATED
|
||||||
relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"])
|
relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"])
|
||||||
model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel")
|
model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/" + MODEL_PATH, Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel")
|
||||||
|
|
||||||
# Attempt to add a thumbnail
|
# Attempt to add a thumbnail
|
||||||
snapshot = self._createSnapshot()
|
snapshot = self._createSnapshot()
|
||||||
|
@ -160,28 +170,32 @@ class ThreeMFWriter(MeshWriter):
|
||||||
thumbnail_buffer.open(QBuffer.OpenModeFlag.ReadWrite)
|
thumbnail_buffer.open(QBuffer.OpenModeFlag.ReadWrite)
|
||||||
snapshot.save(thumbnail_buffer, "PNG")
|
snapshot.save(thumbnail_buffer, "PNG")
|
||||||
|
|
||||||
thumbnail_file = zipfile.ZipInfo("Metadata/thumbnail.png")
|
thumbnail_file = zipfile.ZipInfo(THUMBNAIL_PATH)
|
||||||
# Don't try to compress snapshot file, because the PNG is pretty much as compact as it will get
|
# Don't try to compress snapshot file, because the PNG is pretty much as compact as it will get
|
||||||
archive.writestr(thumbnail_file, thumbnail_buffer.data())
|
archive.writestr(thumbnail_file, thumbnail_buffer.data())
|
||||||
|
|
||||||
# Add PNG to content types file
|
# Add PNG to content types file
|
||||||
thumbnail_type = ET.SubElement(content_types, "Default", Extension = "png", ContentType = "image/png")
|
thumbnail_type = ET.SubElement(content_types, "Default", Extension = "png", ContentType = "image/png")
|
||||||
# Add thumbnail relation to _rels/.rels file
|
# Add thumbnail relation to _rels/.rels file
|
||||||
thumbnail_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/Metadata/thumbnail.png", Id = "rel1", Type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail")
|
thumbnail_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/" + THUMBNAIL_PATH, Id = "rel1", Type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail")
|
||||||
|
|
||||||
|
# Write material metadata
|
||||||
|
material_metadata = self._getMaterialPackageMetadata()
|
||||||
|
self._storeMetadataJson({"packages": material_metadata}, archive, PACKAGE_METADATA_PATH)
|
||||||
|
|
||||||
savitar_scene = Savitar.Scene()
|
savitar_scene = Savitar.Scene()
|
||||||
|
|
||||||
metadata_to_store = CuraApplication.getInstance().getController().getScene().getMetaData()
|
scene_metadata = CuraApplication.getInstance().getController().getScene().getMetaData()
|
||||||
|
|
||||||
for key, value in metadata_to_store.items():
|
for key, value in scene_metadata.items():
|
||||||
savitar_scene.setMetaDataEntry(key, value)
|
savitar_scene.setMetaDataEntry(key, value)
|
||||||
|
|
||||||
current_time_string = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
current_time_string = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
if "Application" not in metadata_to_store:
|
if "Application" not in scene_metadata:
|
||||||
# This might sound a bit strange, but this field should store the original application that created
|
# This might sound a bit strange, but this field should store the original application that created
|
||||||
# the 3mf. So if it was already set, leave it to whatever it was.
|
# the 3mf. So if it was already set, leave it to whatever it was.
|
||||||
savitar_scene.setMetaDataEntry("Application", CuraApplication.getInstance().getApplicationDisplayName())
|
savitar_scene.setMetaDataEntry("Application", CuraApplication.getInstance().getApplicationDisplayName())
|
||||||
if "CreationDate" not in metadata_to_store:
|
if "CreationDate" not in scene_metadata:
|
||||||
savitar_scene.setMetaDataEntry("CreationDate", current_time_string)
|
savitar_scene.setMetaDataEntry("CreationDate", current_time_string)
|
||||||
|
|
||||||
savitar_scene.setMetaDataEntry("ModificationDate", current_time_string)
|
savitar_scene.setMetaDataEntry("ModificationDate", current_time_string)
|
||||||
|
@ -233,6 +247,55 @@ class ThreeMFWriter(MeshWriter):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _storeMetadataJson(metadata: Dict[str, List[Dict[str, str]]], archive: zipfile.ZipFile, path: str) -> None:
|
||||||
|
"""Stores metadata inside archive path as json file"""
|
||||||
|
metadata_file = zipfile.ZipInfo(path)
|
||||||
|
# We have to set the compress type of each file as well (it doesn't keep the type of the entire archive)
|
||||||
|
metadata_file.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
archive.writestr(metadata_file, json.dumps(metadata, separators=(", ", ": "), indent=4, skipkeys=True, ensure_ascii=False))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _getMaterialPackageMetadata() -> List[Dict[str, str]]:
|
||||||
|
"""Get metadata for installed materials in active extruder stack, this does not include bundled materials.
|
||||||
|
|
||||||
|
:return: List of material metadata dictionaries.
|
||||||
|
"""
|
||||||
|
metadata = {}
|
||||||
|
|
||||||
|
package_manager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager())
|
||||||
|
|
||||||
|
for extruder in CuraApplication.getInstance().getExtruderManager().getActiveExtruderStacks():
|
||||||
|
if not extruder.isEnabled:
|
||||||
|
# Don't export materials not in use
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(extruder.material, type(ContainerRegistry.getInstance().getEmptyInstanceContainer())):
|
||||||
|
# This is an empty material container, no material to export
|
||||||
|
continue
|
||||||
|
|
||||||
|
if package_manager.isMaterialBundled(extruder.material.getFileName(), extruder.material.getMetaDataEntry("GUID")):
|
||||||
|
# Don't export bundled materials
|
||||||
|
continue
|
||||||
|
|
||||||
|
package_id = package_manager.getMaterialFilePackageId(extruder.material.getFileName(), extruder.material.getMetaDataEntry("GUID"))
|
||||||
|
package_data = package_manager.getInstalledPackageInfo(package_id)
|
||||||
|
|
||||||
|
# We failed to find the package for this material
|
||||||
|
if not package_data:
|
||||||
|
Logger.info(f"Could not find package for material in extruder {extruder.id}, skipping.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
material_metadata = {"id": package_id,
|
||||||
|
"display_name": package_data.get("display_name") if package_data.get("display_name") else "",
|
||||||
|
"package_version": package_data.get("package_version") if package_data.get("package_version") else "",
|
||||||
|
"sdk_version_semver": package_data.get("sdk_version_semver") if package_data.get("sdk_version_semver") else ""}
|
||||||
|
|
||||||
|
metadata[package_id] = material_metadata
|
||||||
|
|
||||||
|
# Storing in a dict and fetching values to avoid duplicates
|
||||||
|
return list(metadata.values())
|
||||||
|
|
||||||
@call_on_qt_thread # must be called from the main thread because of OpenGL
|
@call_on_qt_thread # must be called from the main thread because of OpenGL
|
||||||
def _createSnapshot(self):
|
def _createSnapshot(self):
|
||||||
Logger.log("d", "Creating thumbnail image...")
|
Logger.log("d", "Creating thumbnail image...")
|
||||||
|
|
|
@ -5,21 +5,23 @@ import sys
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
try:
|
try:
|
||||||
from . import ThreeMFWriter
|
from . import ThreeMFWriter
|
||||||
|
threemf_writer_was_imported = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
Logger.log("w", "Could not import ThreeMFWriter; libSavitar may be missing")
|
Logger.log("w", "Could not import ThreeMFWriter; libSavitar may be missing")
|
||||||
from . import ThreeMFWorkspaceWriter
|
threemf_writer_was_imported = False
|
||||||
|
|
||||||
|
from . import ThreeMFWorkspaceWriter
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Platform import Platform
|
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
def getMetaData():
|
def getMetaData():
|
||||||
workspace_extension = "3mf"
|
workspace_extension = "3mf"
|
||||||
|
|
||||||
metaData = {}
|
metaData = {}
|
||||||
|
|
||||||
if "3MFWriter.ThreeMFWriter" in sys.modules:
|
if threemf_writer_was_imported:
|
||||||
metaData["mesh_writer"] = {
|
metaData["mesh_writer"] = {
|
||||||
"output": [{
|
"output": [{
|
||||||
"extension": "3mf",
|
"extension": "3mf",
|
||||||
|
@ -39,6 +41,7 @@ def getMetaData():
|
||||||
|
|
||||||
return metaData
|
return metaData
|
||||||
|
|
||||||
|
|
||||||
def register(app):
|
def register(app):
|
||||||
if "3MFWriter.ThreeMFWriter" in sys.modules:
|
if "3MFWriter.ThreeMFWriter" in sys.modules:
|
||||||
return {"mesh_writer": ThreeMFWriter.ThreeMFWriter(),
|
return {"mesh_writer": ThreeMFWriter.ThreeMFWriter(),
|
||||||
|
|
|
@ -3,6 +3,6 @@
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Provides support for writing 3MF files.",
|
"description": "Provides support for writing 3MF files.",
|
||||||
"api": 7,
|
"api": 8,
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,5 +3,5 @@
|
||||||
"author": "fieldOfView",
|
"author": "fieldOfView",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Provides support for reading AMF files.",
|
"description": "Provides support for reading AMF files.",
|
||||||
"api": 7
|
"api": 8
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,6 @@
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"description": "Backup and restore your configuration.",
|
"description": "Backup and restore your configuration.",
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"api": 7,
|
"api": 8,
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import QtQuick 2.7
|
||||||
import QtQuick.Controls 2.1
|
import QtQuick.Controls 2.1
|
||||||
import QtQuick.Layouts 1.3
|
import QtQuick.Layouts 1.3
|
||||||
|
|
||||||
import UM 1.3 as UM
|
import UM 1.5 as UM
|
||||||
import Cura 1.1 as Cura
|
import Cura 1.1 as Cura
|
||||||
|
|
||||||
import "../components"
|
import "../components"
|
||||||
|
@ -22,28 +22,23 @@ Item
|
||||||
width: parent.width
|
width: parent.width
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
Label
|
UM.Label
|
||||||
{
|
{
|
||||||
id: backupTitle
|
id: backupTitle
|
||||||
text: catalog.i18nc("@title", "My Backups")
|
text: catalog.i18nc("@title", "My Backups")
|
||||||
font: UM.Theme.getFont("large")
|
font: UM.Theme.getFont("large")
|
||||||
color: UM.Theme.getColor("text")
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
renderType: Text.NativeRendering
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Label
|
UM.Label
|
||||||
{
|
{
|
||||||
text: catalog.i18nc("@empty_state",
|
text: catalog.i18nc("@empty_state",
|
||||||
"You don't have any backups currently. Use the 'Backup Now' button to create one.")
|
"You don't have any backups currently. Use the 'Backup Now' button to create one.")
|
||||||
width: parent.width
|
width: parent.width
|
||||||
font: UM.Theme.getFont("default")
|
|
||||||
color: UM.Theme.getColor("text")
|
|
||||||
wrapMode: Label.WordWrap
|
wrapMode: Label.WordWrap
|
||||||
visible: backupList.model.length == 0
|
visible: backupList.model.length == 0
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
renderType: Text.NativeRendering
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BackupList
|
BackupList
|
||||||
|
@ -54,16 +49,13 @@ Item
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
}
|
}
|
||||||
|
|
||||||
Label
|
UM.Label
|
||||||
{
|
{
|
||||||
text: catalog.i18nc("@backup_limit_info",
|
text: catalog.i18nc("@backup_limit_info",
|
||||||
"During the preview phase, you'll be limited to 5 visible backups. Remove a backup to see older ones.")
|
"During the preview phase, you'll be limited to 5 visible backups. Remove a backup to see older ones.")
|
||||||
width: parent.width
|
width: parent.width
|
||||||
font: UM.Theme.getFont("default")
|
|
||||||
color: UM.Theme.getColor("text")
|
|
||||||
wrapMode: Label.WordWrap
|
wrapMode: Label.WordWrap
|
||||||
visible: backupList.model.length > 4
|
visible: backupList.model.length > 4
|
||||||
renderType: Text.NativeRendering
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BackupListFooter
|
BackupListFooter
|
||||||
|
|
|
@ -23,7 +23,7 @@ Column
|
||||||
{
|
{
|
||||||
id: profileImage
|
id: profileImage
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
source: "../images/backup.svg"
|
source: Qt.resolvedUrl("../images/backup.svg")
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
width: Math.round(parent.width / 4)
|
width: Math.round(parent.width / 4)
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,5 +139,9 @@ message GCodePrefix {
|
||||||
bytes data = 2; //Header string to be prepended before the rest of the g-code sent from the engine.
|
bytes data = 2; //Header string to be prepended before the rest of the g-code sent from the engine.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message SliceUUID {
|
||||||
|
string slice_uuid = 1; //The UUID of the slice.
|
||||||
|
}
|
||||||
|
|
||||||
message SlicingFinished {
|
message SlicingFinished {
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
executable_name = "CuraEngine"
|
executable_name = "CuraEngine"
|
||||||
if Platform.isWindows():
|
if Platform.isWindows():
|
||||||
executable_name += ".exe"
|
executable_name += ".exe"
|
||||||
default_engine_location = executable_name
|
self._default_engine_location = executable_name
|
||||||
|
|
||||||
search_path = [
|
search_path = [
|
||||||
os.path.abspath(os.path.dirname(sys.executable)),
|
os.path.abspath(os.path.dirname(sys.executable)),
|
||||||
|
@ -74,29 +74,29 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
for path in search_path:
|
for path in search_path:
|
||||||
engine_path = os.path.join(path, executable_name)
|
engine_path = os.path.join(path, executable_name)
|
||||||
if os.path.isfile(engine_path):
|
if os.path.isfile(engine_path):
|
||||||
default_engine_location = engine_path
|
self._default_engine_location = engine_path
|
||||||
break
|
break
|
||||||
|
|
||||||
if Platform.isLinux() and not default_engine_location:
|
if Platform.isLinux() and not self._default_engine_location:
|
||||||
if not os.getenv("PATH"):
|
if not os.getenv("PATH"):
|
||||||
raise OSError("There is something wrong with your Linux installation.")
|
raise OSError("There is something wrong with your Linux installation.")
|
||||||
for pathdir in cast(str, os.getenv("PATH")).split(os.pathsep):
|
for pathdir in cast(str, os.getenv("PATH")).split(os.pathsep):
|
||||||
execpath = os.path.join(pathdir, executable_name)
|
execpath = os.path.join(pathdir, executable_name)
|
||||||
if os.path.exists(execpath):
|
if os.path.exists(execpath):
|
||||||
default_engine_location = execpath
|
self._default_engine_location = execpath
|
||||||
break
|
break
|
||||||
|
|
||||||
application = CuraApplication.getInstance() #type: CuraApplication
|
application = CuraApplication.getInstance() #type: CuraApplication
|
||||||
self._multi_build_plate_model = None #type: Optional[MultiBuildPlateModel]
|
self._multi_build_plate_model = None #type: Optional[MultiBuildPlateModel]
|
||||||
self._machine_error_checker = None #type: Optional[MachineErrorChecker]
|
self._machine_error_checker = None #type: Optional[MachineErrorChecker]
|
||||||
|
|
||||||
if not default_engine_location:
|
if not self._default_engine_location:
|
||||||
raise EnvironmentError("Could not find CuraEngine")
|
raise EnvironmentError("Could not find CuraEngine")
|
||||||
|
|
||||||
Logger.log("i", "Found CuraEngine at: %s", default_engine_location)
|
Logger.log("i", "Found CuraEngine at: %s", self._default_engine_location)
|
||||||
|
|
||||||
default_engine_location = os.path.abspath(default_engine_location)
|
self._default_engine_location = os.path.abspath(self._default_engine_location)
|
||||||
application.getPreferences().addPreference("backend/location", default_engine_location)
|
application.getPreferences().addPreference("backend/location", self._default_engine_location)
|
||||||
|
|
||||||
# Workaround to disable layer view processing if layer view is not active.
|
# Workaround to disable layer view processing if layer view is not active.
|
||||||
self._layer_view_active = False #type: bool
|
self._layer_view_active = False #type: bool
|
||||||
|
@ -124,6 +124,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
self._message_handlers["cura.proto.Progress"] = self._onProgressMessage
|
self._message_handlers["cura.proto.Progress"] = self._onProgressMessage
|
||||||
self._message_handlers["cura.proto.GCodeLayer"] = self._onGCodeLayerMessage
|
self._message_handlers["cura.proto.GCodeLayer"] = self._onGCodeLayerMessage
|
||||||
self._message_handlers["cura.proto.GCodePrefix"] = self._onGCodePrefixMessage
|
self._message_handlers["cura.proto.GCodePrefix"] = self._onGCodePrefixMessage
|
||||||
|
self._message_handlers["cura.proto.SliceUUID"] = self._onSliceUUIDMessage
|
||||||
self._message_handlers["cura.proto.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates
|
self._message_handlers["cura.proto.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates
|
||||||
self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage
|
self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage
|
||||||
|
|
||||||
|
@ -215,7 +216,12 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
This is useful for debugging and used to actually start the engine.
|
This is useful for debugging and used to actually start the engine.
|
||||||
:return: list of commands and args / parameters.
|
:return: list of commands and args / parameters.
|
||||||
"""
|
"""
|
||||||
command = [CuraApplication.getInstance().getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), ""]
|
from cura import ApplicationMetadata
|
||||||
|
if ApplicationMetadata.IsEnterpriseVersion:
|
||||||
|
command = [self._default_engine_location]
|
||||||
|
else:
|
||||||
|
command = [CuraApplication.getInstance().getPreferences().getValue("backend/location")]
|
||||||
|
command += ["connect", "127.0.0.1:{0}".format(self._port), ""]
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(prog = "cura", add_help = False)
|
parser = argparse.ArgumentParser(prog = "cura", add_help = False)
|
||||||
parser.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.")
|
parser.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.")
|
||||||
|
@ -807,6 +813,10 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
|
except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
|
||||||
pass # Throw the message away.
|
pass # Throw the message away.
|
||||||
|
|
||||||
|
def _onSliceUUIDMessage(self, message: Arcus.PythonMessage) -> None:
|
||||||
|
application = CuraApplication.getInstance()
|
||||||
|
application.getPrintInformation().slice_uuid = message.slice_uuid
|
||||||
|
|
||||||
def _createSocket(self, protocol_file: str = None) -> None:
|
def _createSocket(self, protocol_file: str = None) -> None:
|
||||||
"""Creates a new socket connection."""
|
"""Creates a new socket connection."""
|
||||||
|
|
||||||
|
|
|
@ -369,6 +369,9 @@ class StartSliceJob(Job):
|
||||||
result["material_name"] = stack.material.getMetaDataEntry("name", "")
|
result["material_name"] = stack.material.getMetaDataEntry("name", "")
|
||||||
result["material_brand"] = stack.material.getMetaDataEntry("brand", "")
|
result["material_brand"] = stack.material.getMetaDataEntry("brand", "")
|
||||||
|
|
||||||
|
result["quality_name"] = stack.quality.getMetaDataEntry("name", "")
|
||||||
|
result["quality_changes_name"] = stack.qualityChanges.getMetaDataEntry("name")
|
||||||
|
|
||||||
# Renamed settings.
|
# Renamed settings.
|
||||||
result["print_bed_temperature"] = result["material_bed_temperature"]
|
result["print_bed_temperature"] = result["material_bed_temperature"]
|
||||||
result["print_temperature"] = result["material_print_temperature"]
|
result["print_temperature"] = result["material_print_temperature"]
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "CuraEngine Backend",
|
"name": "CuraEngine Backend",
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"description": "Provides the link to the CuraEngine slicing backend.",
|
"description": "Provides the link to the CuraEngine slicing backend.",
|
||||||
"api": 7,
|
"api": 8,
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,6 @@
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Provides support for importing Cura profiles.",
|
"description": "Provides support for importing Cura profiles.",
|
||||||
"api": 7,
|
"api": 8,
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,6 @@
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Provides support for exporting Cura profiles.",
|
"description": "Provides support for exporting Cura profiles.",
|
||||||
"api": 7,
|
"api": 8,
|
||||||
"i18n-catalog":"cura"
|
"i18n-catalog":"cura"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,6 @@
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"description": "Connects to the Digital Library, allowing Cura to open files from and save files to the Digital Library.",
|
"description": "Connects to the Digital Library, allowing Cura to open files from and save files to the Digital Library.",
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"api": 7,
|
"api": 8,
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import QtQuick 2.15
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.2
|
||||||
import QtQuick.Controls 2.3
|
import QtQuick.Controls 2.3
|
||||||
|
|
||||||
import UM 1.2 as UM
|
import UM 1.5 as UM
|
||||||
import Cura 1.6 as Cura
|
import Cura 1.6 as Cura
|
||||||
|
|
||||||
import DigitalFactory 1.0 as DF
|
import DigitalFactory 1.0 as DF
|
||||||
|
@ -64,12 +64,10 @@ Popup
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label
|
UM.Label
|
||||||
{
|
{
|
||||||
id: projectNameLabel
|
id: projectNameLabel
|
||||||
text: "Project Name"
|
text: "Project Name"
|
||||||
font: UM.Theme.getFont("default")
|
|
||||||
color: UM.Theme.getColor("text")
|
|
||||||
anchors
|
anchors
|
||||||
{
|
{
|
||||||
top: createNewLibraryProjectLabel.bottom
|
top: createNewLibraryProjectLabel.bottom
|
||||||
|
@ -107,13 +105,12 @@ Popup
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label
|
UM.Label
|
||||||
{
|
{
|
||||||
id: errorWhileCreatingProjectLabel
|
id: errorWhileCreatingProjectLabel
|
||||||
text: manager.projectCreationErrorText
|
text: manager.projectCreationErrorText
|
||||||
width: parent.width
|
width: parent.width
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
font: UM.Theme.getFont("default")
|
|
||||||
color: UM.Theme.getColor("error")
|
color: UM.Theme.getColor("error")
|
||||||
visible: manager.creatingNewProjectStatus == DF.RetrievalStatus.Failed
|
visible: manager.creatingNewProjectStatus == DF.RetrievalStatus.Failed
|
||||||
anchors
|
anchors
|
||||||
|
|
|
@ -37,7 +37,7 @@ Cura.RoundedRectangle
|
||||||
width: UM.Theme.getSize("section").height
|
width: UM.Theme.getSize("section").height
|
||||||
height: width
|
height: width
|
||||||
color: UM.Theme.getColor("text_link")
|
color: UM.Theme.getColor("text_link")
|
||||||
source: "../images/arrow_down.svg"
|
source: Qt.resolvedUrl("../images/arrow_down.svg")
|
||||||
}
|
}
|
||||||
|
|
||||||
Label
|
Label
|
||||||
|
@ -65,7 +65,7 @@ Cura.RoundedRectangle
|
||||||
{
|
{
|
||||||
target: projectImage
|
target: projectImage
|
||||||
color: UM.Theme.getColor("text_link")
|
color: UM.Theme.getColor("text_link")
|
||||||
source: "../images/arrow_down.svg"
|
source: Qt.resolvedUrl("../images/arrow_down.svg")
|
||||||
}
|
}
|
||||||
PropertyChanges
|
PropertyChanges
|
||||||
{
|
{
|
||||||
|
@ -88,7 +88,7 @@ Cura.RoundedRectangle
|
||||||
{
|
{
|
||||||
target: projectImage
|
target: projectImage
|
||||||
color: UM.Theme.getColor("text_link")
|
color: UM.Theme.getColor("text_link")
|
||||||
source: "../images/arrow_down.svg"
|
source: Qt.resolvedUrl("../images/arrow_down.svg")
|
||||||
}
|
}
|
||||||
PropertyChanges
|
PropertyChanges
|
||||||
{
|
{
|
||||||
|
@ -111,7 +111,7 @@ Cura.RoundedRectangle
|
||||||
{
|
{
|
||||||
target: projectImage
|
target: projectImage
|
||||||
color: UM.Theme.getColor("action_button_disabled_text")
|
color: UM.Theme.getColor("action_button_disabled_text")
|
||||||
source: "../images/update.svg"
|
source: Qt.resolvedUrl("../images/update.svg")
|
||||||
}
|
}
|
||||||
PropertyChanges
|
PropertyChanges
|
||||||
{
|
{
|
||||||
|
|
|
@ -83,13 +83,12 @@ Item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label
|
UM.Label
|
||||||
{
|
{
|
||||||
id: emptyProjectLabel
|
id: emptyProjectLabel
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: "Select a project to view its files."
|
text: "Select a project to view its files."
|
||||||
font: UM.Theme.getFont("default")
|
|
||||||
color: UM.Theme.getColor("setting_category_text")
|
color: UM.Theme.getColor("setting_category_text")
|
||||||
|
|
||||||
Connections
|
Connections
|
||||||
|
@ -102,14 +101,13 @@ Item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label
|
UM.Label
|
||||||
{
|
{
|
||||||
id: noFilesInProjectLabel
|
id: noFilesInProjectLabel
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: (manager.digitalFactoryFileModel.count == 0 && !emptyProjectLabel.visible && !retrievingFilesBusyIndicator.visible)
|
visible: (manager.digitalFactoryFileModel.count == 0 && !emptyProjectLabel.visible && !retrievingFilesBusyIndicator.visible)
|
||||||
text: "No supported files in this project."
|
text: "No supported files in this project."
|
||||||
font: UM.Theme.getFont("default")
|
|
||||||
color: UM.Theme.getColor("setting_category_text")
|
color: UM.Theme.getColor("setting_category_text")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import QtQuick 2.10
|
import QtQuick 2.10
|
||||||
import QtQuick.Controls 2.3
|
import QtQuick.Controls 2.3
|
||||||
|
|
||||||
import UM 1.2 as UM
|
import UM 1.5 as UM
|
||||||
import Cura 1.6 as Cura
|
import Cura 1.6 as Cura
|
||||||
|
|
||||||
Cura.RoundedRectangle
|
Cura.RoundedRectangle
|
||||||
|
@ -58,34 +58,31 @@ Cura.RoundedRectangle
|
||||||
width: parent.width - x - UM.Theme.getSize("default_margin").width
|
width: parent.width - x - UM.Theme.getSize("default_margin").width
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
Label
|
UM.Label
|
||||||
{
|
{
|
||||||
id: displayNameLabel
|
id: displayNameLabel
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: Math.round(parent.height / 3)
|
height: Math.round(parent.height / 3)
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
color: UM.Theme.getColor("text")
|
|
||||||
font: UM.Theme.getFont("default_bold")
|
font: UM.Theme.getFont("default_bold")
|
||||||
}
|
}
|
||||||
|
|
||||||
Label
|
UM.Label
|
||||||
{
|
{
|
||||||
id: usernameLabel
|
id: usernameLabel
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: Math.round(parent.height / 3)
|
height: Math.round(parent.height / 3)
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
color: UM.Theme.getColor("small_button_text")
|
color: UM.Theme.getColor("small_button_text")
|
||||||
font: UM.Theme.getFont("default")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Label
|
UM.Label
|
||||||
{
|
{
|
||||||
id: lastUpdatedLabel
|
id: lastUpdatedLabel
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: Math.round(parent.height / 3)
|
height: Math.round(parent.height / 3)
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
color: UM.Theme.getColor("small_button_text")
|
color: UM.Theme.getColor("small_button_text")
|
||||||
font: UM.Theme.getFont("default")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,9 @@ import DigitalFactory 1.0 as DF
|
||||||
Item
|
Item
|
||||||
{
|
{
|
||||||
id: base
|
id: base
|
||||||
|
|
||||||
|
property variant catalog: UM.I18nCatalog { name: "cura" }
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height
|
height: parent.height
|
||||||
|
|
||||||
|
@ -44,14 +47,13 @@ Item
|
||||||
cardMouseAreaEnabled: false
|
cardMouseAreaEnabled: false
|
||||||
}
|
}
|
||||||
|
|
||||||
Label
|
UM.Label
|
||||||
{
|
{
|
||||||
id: fileNameLabel
|
id: fileNameLabel
|
||||||
anchors.top: projectSummaryCard.bottom
|
anchors.top: projectSummaryCard.bottom
|
||||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||||
text: "Cura project name"
|
text: "Cura project name"
|
||||||
font: UM.Theme.getFont("medium")
|
font: UM.Theme.getFont("medium")
|
||||||
color: UM.Theme.getColor("text")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -110,13 +112,12 @@ Item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label
|
UM.Label
|
||||||
{
|
{
|
||||||
id: emptyProjectLabel
|
id: emptyProjectLabel
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: "Select a project to view its files."
|
text: "Select a project to view its files."
|
||||||
font: UM.Theme.getFont("default")
|
|
||||||
color: UM.Theme.getColor("setting_category_text")
|
color: UM.Theme.getColor("setting_category_text")
|
||||||
|
|
||||||
Connections
|
Connections
|
||||||
|
@ -129,14 +130,13 @@ Item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label
|
UM.Label
|
||||||
{
|
{
|
||||||
id: noFilesInProjectLabel
|
id: noFilesInProjectLabel
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
visible: (manager.digitalFactoryFileModel.count == 0 && !emptyProjectLabel.visible && !retrievingFilesBusyIndicator.visible)
|
visible: (manager.digitalFactoryFileModel.count == 0 && !emptyProjectLabel.visible && !retrievingFilesBusyIndicator.visible)
|
||||||
text: "No supported files in this project."
|
text: "No supported files in this project."
|
||||||
font: UM.Theme.getFont("default")
|
|
||||||
color: UM.Theme.getColor("setting_category_text")
|
color: UM.Theme.getColor("setting_category_text")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,53 +193,29 @@ Item
|
||||||
text: "Save"
|
text: "Save"
|
||||||
enabled: (asProjectCheckbox.checked || asSlicedCheckbox.checked) && dfFilenameTextfield.text.length >= 1 && dfFilenameTextfield.state !== 'invalid'
|
enabled: (asProjectCheckbox.checked || asSlicedCheckbox.checked) && dfFilenameTextfield.text.length >= 1 && dfFilenameTextfield.state !== 'invalid'
|
||||||
|
|
||||||
onClicked:
|
onClicked: manager.saveFileToSelectedProject(dfFilenameTextfield.text, asProjectComboBox.currentValue)
|
||||||
{
|
|
||||||
let saveAsFormats = [];
|
|
||||||
if (asProjectCheckbox.checked)
|
|
||||||
{
|
|
||||||
saveAsFormats.push("3mf");
|
|
||||||
}
|
|
||||||
if (asSlicedCheckbox.checked)
|
|
||||||
{
|
|
||||||
saveAsFormats.push("ufp");
|
|
||||||
}
|
|
||||||
manager.saveFileToSelectedProject(dfFilenameTextfield.text, saveAsFormats);
|
|
||||||
}
|
|
||||||
busy: false
|
busy: false
|
||||||
}
|
}
|
||||||
|
|
||||||
Row
|
Cura.ComboBox
|
||||||
{
|
{
|
||||||
|
id: asProjectComboBox
|
||||||
|
|
||||||
id: saveAsFormatRow
|
width: UM.Theme.getSize("combobox_wide").width
|
||||||
|
height: saveButton.height
|
||||||
anchors.verticalCenter: saveButton.verticalCenter
|
anchors.verticalCenter: saveButton.verticalCenter
|
||||||
anchors.right: saveButton.left
|
anchors.right: saveButton.left
|
||||||
anchors.rightMargin: UM.Theme.getSize("thin_margin").height
|
anchors.rightMargin: UM.Theme.getSize("thin_margin").height
|
||||||
width: childrenRect.width
|
|
||||||
spacing: UM.Theme.getSize("default_margin").width
|
|
||||||
|
|
||||||
UM.CheckBox
|
enabled: UM.Backend.state == UM.Backend.Done
|
||||||
{
|
currentIndex: UM.Backend.state == UM.Backend.Done ? 0 : 1
|
||||||
id: asProjectCheckbox
|
textRole: "text"
|
||||||
height: UM.Theme.getSize("checkbox").height
|
valueRole: "value"
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
checked: true
|
|
||||||
text: "Save Cura project"
|
|
||||||
font: UM.Theme.getFont("medium")
|
|
||||||
}
|
|
||||||
|
|
||||||
UM.CheckBox
|
model: [
|
||||||
{
|
{ text: catalog.i18nc("@option", "Save Cura project and print file"), key: "3mf_ufp", value: ["3mf", "ufp"] },
|
||||||
id: asSlicedCheckbox
|
{ text: catalog.i18nc("@option", "Save Cura project"), key: "3mf", value: ["3mf"] },
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted:
|
Component.onCompleted:
|
||||||
|
|