Make Conan/Python installs available for whole project and not just the AboutDialog

Generation of dependency list now happens in
Also cleaned up the AboutDialog.qml

CURA-10561
This commit is contained in:
c.lamboo 2023-10-19 15:48:28 +02:00
parent 5f998f0ab4
commit 6f1adaad43
10 changed files with 287 additions and 369 deletions

View file

@ -155,7 +155,7 @@ jobs:
run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache" run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache"
- name: Create the Packages (Bash) - name: Create the Packages (Bash)
run: conan install $CURA_CONAN_VERSION ${{ inputs.conan_args }} --build=missing --update -if cura_inst -g VirtualPythonEnv -o cura:enterprise=$ENTERPRISE -o cura:staging=$STAGING --json "cura_inst/conan_install_info.json" run: conan install $CURA_CONAN_VERSION ${{ inputs.conan_args }} --build=missing --update -if cura_inst -g VirtualPythonEnv -o cura:enterprise=$ENTERPRISE -o cura:staging=$STAGING
- name: Upload the Package(s) - name: Upload the Package(s)
if: always() if: always()
@ -206,12 +206,7 @@ jobs:
import json import json
from pathlib import Path from pathlib import Path
conan_install_info_path = Path("cura_inst/conan_install_info.json") from cura.CuraVersion import ConanInstalls
conan_info = {"installed": []}
if os.path.exists(conan_install_info_path):
with open(conan_install_info_path, "r") as f:
conan_info = json.load(f)
sorted_deps = sorted([dep["recipe"]["id"].replace('#', r' rev: ') for dep in conan_info["installed"]])
summary_env = os.environ["GITHUB_STEP_SUMMARY"] summary_env = os.environ["GITHUB_STEP_SUMMARY"]
content = "" content = ""
@ -223,14 +218,17 @@ jobs:
f.write(content) f.write(content)
f.writelines("# ${{ steps.filename.outputs.INSTALLER_FILENAME }}\n") f.writelines("# ${{ steps.filename.outputs.INSTALLER_FILENAME }}\n")
f.writelines("## Conan packages:\n") f.writelines("## Conan packages:\n")
for dep in sorted_deps: for dep_name, dep_info in ConanDependencies.items():
f.writelines(f"`{dep}`\n") f.writelines(f"`{dep_name} {dep_info["version"]} {dep_info["revision"]}`\n")
- name: Summarize the used Python modules - name: Summarize the used Python modules
shell: python shell: python
run: | run: |
import os import os
import pkg_resources import pkg_resources
from cura.CuraVersion import ConanDependencies
summary_env = os.environ["GITHUB_STEP_SUMMARY"] summary_env = os.environ["GITHUB_STEP_SUMMARY"]
content = "" content = ""
if os.path.exists(summary_env): if os.path.exists(summary_env):
@ -240,8 +238,8 @@ jobs:
with open(summary_env, "w") as f: with open(summary_env, "w") as f:
f.write(content) f.write(content)
f.writelines("## Python modules:\n") f.writelines("## Python modules:\n")
for package in pkg_resources.working_set: for dep_name, dep_info in ConanDependencies.items():
f.writelines(f"`{package.key}/{package.version}`\n") f.writelines(f"`{dep_name} {dep_info["version"]}`\n")
- name: Create the Linux AppImage (Bash) - name: Create the Linux AppImage (Bash)
run: | run: |

View file

@ -155,7 +155,7 @@ jobs:
run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache" run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache"
- name: Create the Packages (Bash) - name: Create the Packages (Bash)
run: conan install $CURA_CONAN_VERSION ${{ inputs.conan_args }} --build=missing --update -if cura_inst -g VirtualPythonEnv -o cura:enterprise=$ENTERPRISE -o cura:staging=$STAGING --json "cura_inst/conan_install_info.json" run: conan install $CURA_CONAN_VERSION ${{ inputs.conan_args }} --build=missing --update -if cura_inst -g VirtualPythonEnv -o cura:enterprise=$ENTERPRISE -o cura:staging=$STAGING"
- name: Upload the Package(s) - name: Upload the Package(s)
if: ${{ inputs.operating_system != 'self-hosted' }} if: ${{ inputs.operating_system != 'self-hosted' }}
@ -210,12 +210,7 @@ jobs:
import json import json
from pathlib import Path from pathlib import Path
conan_install_info_path = Path("cura_inst/conan_install_info.json") from cura.CuraVersion import ConanInstalls
conan_info = {"installed": []}
if os.path.exists(conan_install_info_path):
with open(conan_install_info_path, "r") as f:
conan_info = json.load(f)
sorted_deps = sorted([dep["recipe"]["id"].replace('#', r' rev: ') for dep in conan_info["installed"]])
summary_env = os.environ["GITHUB_STEP_SUMMARY"] summary_env = os.environ["GITHUB_STEP_SUMMARY"]
content = "" content = ""
@ -227,14 +222,17 @@ jobs:
f.write(content) f.write(content)
f.writelines("# ${{ steps.filename.outputs.INSTALLER_FILENAME }}\n") f.writelines("# ${{ steps.filename.outputs.INSTALLER_FILENAME }}\n")
f.writelines("## Conan packages:\n") f.writelines("## Conan packages:\n")
for dep in sorted_deps: for dep_name, dep_info in ConanDependencies.items():
f.writelines(f"`{dep}`\n") f.writelines(f"`{dep_name} {dep_info["version"]} {dep_info["revision"]}`\n")
- name: Summarize the used Python modules - name: Summarize the used Python modules
shell: python shell: python
run: | run: |
import os import os
import pkg_resources import pkg_resources
from cura.CuraVersion import PythonInstalls
summary_env = os.environ["GITHUB_STEP_SUMMARY"] summary_env = os.environ["GITHUB_STEP_SUMMARY"]
content = "" content = ""
if os.path.exists(summary_env): if os.path.exists(summary_env):
@ -244,8 +242,8 @@ jobs:
with open(summary_env, "w") as f: with open(summary_env, "w") as f:
f.write(content) f.write(content)
f.writelines("## Python modules:\n") f.writelines("## Python modules:\n")
for package in pkg_resources.working_set: for dep_name, dep_info in ConanDependencies.items():
f.writelines(f"`{package.key}/{package.version}`\n") f.writelines(f"`{dep_name} {dep_info["version"]}`\n")
- name: Create the Macos dmg (Bash) - name: Create the Macos dmg (Bash)
run: python ../cura_inst/packaging/MacOS/build_macos.py --source_path ../cura_inst --dist_path . --cura_conan_version $CURA_CONAN_VERSION --filename "${{ steps.filename.outputs.INSTALLER_FILENAME }}" --build_dmg --build_pkg --app_name "$CURA_APP_NAME" run: python ../cura_inst/packaging/MacOS/build_macos.py --source_path ../cura_inst --dist_path . --cura_conan_version $CURA_CONAN_VERSION --filename "${{ steps.filename.outputs.INSTALLER_FILENAME }}" --build_dmg --build_pkg --app_name "$CURA_APP_NAME"

View file

@ -122,7 +122,7 @@ jobs:
run: conan config set storage.download_cache="C:\Users\runneradmin\.conan\conan_download_cache" run: conan config set storage.download_cache="C:\Users\runneradmin\.conan\conan_download_cache"
- name: Create the Packages (Powershell) - name: Create the Packages (Powershell)
run: conan install $Env:CURA_CONAN_VERSION ${{ inputs.conan_args }} --build=missing --update -if cura_inst -g VirtualPythonEnv -o cura:enterprise=$Env:ENTERPRISE -o cura:staging=$Env:STAGING --json "cura_inst/conan_install_info.json" run: conan install $Env:CURA_CONAN_VERSION ${{ inputs.conan_args }} --build=missing --update -if cura_inst -g VirtualPythonEnv -o cura:enterprise=$Env:ENTERPRISE -o cura:staging=$Env:STAGING
- name: Upload the Package(s) - name: Upload the Package(s)
if: always() if: always()
@ -169,12 +169,7 @@ jobs:
import json import json
from pathlib import Path from pathlib import Path
conan_install_info_path = Path("cura_inst/conan_install_info.json") from cura.CuraVersion import ConanInstalls
conan_info = {"installed": []}
if os.path.exists(conan_install_info_path):
with open(conan_install_info_path, "r") as f:
conan_info = json.load(f)
sorted_deps = sorted([dep["recipe"]["id"].replace('#', r' rev: ') for dep in conan_info["installed"]])
summary_env = os.environ["GITHUB_STEP_SUMMARY"] summary_env = os.environ["GITHUB_STEP_SUMMARY"]
content = "" content = ""
@ -186,14 +181,17 @@ jobs:
f.write(content) f.write(content)
f.writelines("# ${{ steps.filename.outputs.INSTALLER_FILENAME }}\n") f.writelines("# ${{ steps.filename.outputs.INSTALLER_FILENAME }}\n")
f.writelines("## Conan packages:\n") f.writelines("## Conan packages:\n")
for dep in sorted_deps: for dep_name, dep_info in ConanDependencies.items():
f.writelines(f"`{dep}`\n") f.writelines(f"`{dep_name} {dep_info["version"]} {dep_info["revision"]}`\n")
- name: Summarize the used Python modules - name: Summarize the used Python modules
shell: python shell: python
run: | run: |
import os import os
import pkg_resources import pkg_resources
from cura.CuraVersion import PythonInstalls
summary_env = os.environ["GITHUB_STEP_SUMMARY"] summary_env = os.environ["GITHUB_STEP_SUMMARY"]
content = "" content = ""
if os.path.exists(summary_env): if os.path.exists(summary_env):
@ -203,8 +201,8 @@ jobs:
with open(summary_env, "w") as f: with open(summary_env, "w") as f:
f.write(content) f.write(content)
f.writelines("## Python modules:\n") f.writelines("## Python modules:\n")
for package in pkg_resources.working_set: for dep_name, dep_info in ConanDependencies.items():
f.writelines(f"`{package.key}/{package.version}`\n") f.writelines(f"`{dep_name} {dep_info["version"]}`\n")
- name: Create PFX certificate from BASE64_PFX_CONTENT secret - name: Create PFX certificate from BASE64_PFX_CONTENT secret
id: create-pfx id: create-pfx

1
.gitignore vendored
View file

@ -101,7 +101,6 @@ graph_info.json
Ultimaker-Cura.spec Ultimaker-Cura.spec
.run/ .run/
/printer-linter/src/printerlinter.egg-info/ /printer-linter/src/printerlinter.egg-info/
/resources/qml/Dialogs/AboutDialogVersionsList.qml
/plugins/CuraEngineGradualFlow /plugins/CuraEngineGradualFlow
/resources/bundled_packages/bundled_*.json /resources/bundled_packages/bundled_*.json
curaengine_plugin_gradual_flow curaengine_plugin_gradual_flow

View file

@ -1,61 +0,0 @@
import QtQuick 2.2
import QtQuick.Controls 2.9
import UM 1.6 as UM
import Cura 1.5 as Cura
ListView
{
id: projectBuildInfoList
visible: false
anchors.top: creditsNotes.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width
height: base.height - y - (2 * UM.Theme.getSize("default_margin").height + closeButton.height)
ScrollBar.vertical: UM.ScrollBar
{
id: projectBuildInfoListScrollBar
}
delegate: Row
{
spacing: UM.Theme.getSize("narrow_margin").width
UM.Label
{
text: (model.name)
width: (projectBuildInfoList.width* 0.4) | 0
elide: Text.ElideRight
}
UM.Label
{
text: (model.version)
width: (projectBuildInfoList.width *0.6) | 0
elide: Text.ElideRight
}
}
model: ListModel
{
id: developerInfo
}
Component.onCompleted:
{
var conan_installs = {{ conan_installs }};
var python_installs = {{ python_installs }};
developerInfo.append({ name : "<H1>Conan Installs</H1>", version : '' });
for (var n in conan_installs)
{
developerInfo.append({ name : conan_installs[n][0], version : conan_installs[n][1] });
}
developerInfo.append({ name : '', version : '' });
developerInfo.append({ name : "<H1>Python Installs</H1>", version : '' });
for (var n in python_installs)
{
developerInfo.append({ name : python_installs[n][0], version : python_installs[n][1] });
}
}
}

View file

@ -1,4 +1,4 @@
# Copyright (c) 2022 UltiMaker # Copyright (c) 2023 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
CuraAppName = "{{ cura_app_name }}" CuraAppName = "{{ cura_app_name }}"
@ -12,3 +12,6 @@ CuraCloudAccountAPIRoot = "{{ cura_cloud_account_api_root }}"
CuraMarketplaceRoot = "{{ cura_marketplace_root }}" CuraMarketplaceRoot = "{{ cura_marketplace_root }}"
CuraDigitalFactoryURL = "{{ cura_digital_factory_url }}" CuraDigitalFactoryURL = "{{ cura_digital_factory_url }}"
CuraLatestURL = "{{ cura_latest_url }}" CuraLatestURL = "{{ cura_latest_url }}"
ConanInstalls = {{ conan_installs }}
PythonInstalls = {{ python_installs }}

View file

@ -137,18 +137,21 @@ class CuraConan(ConanFile):
return "'x86_64'" return "'x86_64'"
return "None" return "None"
def _generate_about_versions(self, location): def _conan_installs(self):
with open(os.path.join(self.recipe_folder, "AboutDialogVersionsList.qml.jinja"), "r") as f: conan_installs = {}
cura_version_py = Template(f.read())
conan_installs = [] # list of conan installs
python_installs = [] for dependency in self.dependencies.host.values():
conan_installs[dependency.ref.name] = {
"version": dependency.ref.version,
"revision": dependency.ref.revision
}
return conan_installs
# list of conan installs def _python_installs(self):
for _, dependency in self.dependencies.host.items(): python_installs = {}
conan_installs.append([dependency.ref.name,dependency.ref.version])
#list of python installs # list of python installs
outer = '"' if self.settings.os == "Windows" else "'" outer = '"' if self.settings.os == "Windows" else "'"
inner = "'" if self.settings.os == "Windows" else '"' inner = "'" if self.settings.os == "Windows" else '"'
python_ins_cmd = f"python -c {outer}import pkg_resources; print({inner};{inner}.join([(s.key+{inner},{inner}+ s.version) for s in pkg_resources.working_set])){outer}" python_ins_cmd = f"python -c {outer}import pkg_resources; print({inner};{inner}.join([(s.key+{inner},{inner}+ s.version) for s in pkg_resources.working_set])){outer}"
@ -157,16 +160,12 @@ class CuraConan(ConanFile):
self.run(python_ins_cmd, run_environment= True, env = "conanrun", output=buffer) self.run(python_ins_cmd, run_environment= True, env = "conanrun", output=buffer)
packages = str(buffer.getvalue()).split("-----------------\n") packages = str(buffer.getvalue()).split("-----------------\n")
package = packages[1].strip('\r\n').split(";") packages = packages[1].strip('\r\n').split(";")
for pack in package: for package in packages:
python_installs.append(pack.split(",")) name, version = package.split(",")
python_installs[name] = {"version": version}
with open(os.path.join(location, "AboutDialogVersionsList.qml"), "w") as f:
f.write(cura_version_py.render(
conan_installs = conan_installs,
python_installs = python_installs
))
return python_installs
def _generate_cura_version(self, location): def _generate_cura_version(self, location):
with open(os.path.join(self.recipe_folder, "CuraVersion.py.jinja"), "r") as f: with open(os.path.join(self.recipe_folder, "CuraVersion.py.jinja"), "r") as f:
@ -192,89 +191,9 @@ class CuraConan(ConanFile):
cura_cloud_account_api_root = self.conan_data["urls"][self._urls]["cloud_account_api_root"], cura_cloud_account_api_root = self.conan_data["urls"][self._urls]["cloud_account_api_root"],
cura_marketplace_root = self.conan_data["urls"][self._urls]["marketplace_root"], cura_marketplace_root = self.conan_data["urls"][self._urls]["marketplace_root"],
cura_digital_factory_url = self.conan_data["urls"][self._urls]["digital_factory_url"], cura_digital_factory_url = self.conan_data["urls"][self._urls]["digital_factory_url"],
cura_latest_url = self.conan_data["urls"][self._urls]["cura_latest_url"])) cura_latest_url=self.conan_data["urls"][self._urls]["cura_latest_url"],
conan_installs=self._conan_installs(),
def _generate_pyinstaller_spec(self, location, entrypoint_location, icon_path, entitlements_file): python_installs=self._python_installs(),
pyinstaller_metadata = self.conan_data["pyinstaller"]
datas = [(str(self._base_dir.joinpath("conan_install_info.json")), ".")]
for data in pyinstaller_metadata["datas"].values():
if not self.options.internal and data.get("internal", False):
continue
if "package" in data: # get the paths from conan package
if data["package"] == self.name:
if self.in_local_cache:
src_path = os.path.join(self.package_folder, data["src"])
else:
src_path = os.path.join(self.source_folder, data["src"])
else:
src_path = os.path.join(self.deps_cpp_info[data["package"]].rootpath, data["src"])
elif "root" in data: # get the paths relative from the install folder
src_path = os.path.join(self.install_folder, data["root"], data["src"])
else:
continue
if Path(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 = os.path.join(self.deps_cpp_info[binary["package"]].rootpath, binary["src"])
elif "root" in binary: # get the paths relative from the sourcefolder
src_path = str(self.source_path.joinpath(binary["root"], binary["src"]))
if self.settings.os == "Windows":
src_path = src_path.replace("\\", "\\\\")
else:
continue
if not Path(src_path).exists():
self.output.warning(f"Source path for binary {binary['binary']} does not exist")
continue
for bin in Path(src_path).glob(binary["binary"] + "*[.exe|.dll|.so|.dylib|.so.]*"):
binaries.append((str(bin), binary["dst"]))
for bin in Path(src_path).glob(binary["binary"]):
binaries.append((str(bin), binary["dst"]))
# Make sure all Conan dependencies which are shared are added to the binary list for pyinstaller
for _, dependency in self.dependencies.host.items():
for bin_paths in dependency.cpp_info.bindirs:
binaries.extend([(f"{p}", ".") for p in Path(bin_paths).glob("**/*.dll")])
for lib_paths in dependency.cpp_info.libdirs:
binaries.extend([(f"{p}", ".") for p in Path(lib_paths).glob("**/*.so*")])
binaries.extend([(f"{p}", ".") for p in Path(lib_paths).glob("**/*.dylib*")])
# Copy dynamic libs from lib path
binaries.extend([(f"{p}", ".") for p in Path(self._base_dir.joinpath("lib")).glob("**/*.dylib*")])
binaries.extend([(f"{p}", ".") for p in Path(self._base_dir.joinpath("lib")).glob("**/*.so*")])
# 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(os.path.join(self.recipe_folder, "UltiMaker-Cura.spec.jinja"), "r") as f:
pyinstaller = Template(f.read())
version = self.conf_info.get("user.cura:version", default = self.version, check_type = str)
cura_version = Version(version)
with open(os.path.join(location, "UltiMaker-Cura.spec"), "w") as f:
f.write(pyinstaller.render(
name = str(self.options.display_name).replace(" ", "-"),
display_name = self._app_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 = self._pyinstaller_spec_arch,
macos = self.settings.os == "Macos",
version = f"'{version}'",
short_version = f"'{cura_version.major}.{cura_version.minor}.{cura_version.patch}'",
)) ))
def export_sources(self): def export_sources(self):
@ -346,7 +265,6 @@ class CuraConan(ConanFile):
vr.generate() vr.generate()
self._generate_cura_version(os.path.join(self.source_folder, "cura")) self._generate_cura_version(os.path.join(self.source_folder, "cura"))
self._generate_about_versions(os.path.join(self.source_folder, "resources","qml", "Dialogs"))
if not self.in_local_cache: if not self.in_local_cache:
# Copy CuraEngine.exe to bindirs of Virtual Python Environment # Copy CuraEngine.exe to bindirs of Virtual Python Environment
@ -387,12 +305,6 @@ class CuraConan(ConanFile):
copy(self, "*", cura_private_data.resdirs[0], str(self._share_dir.joinpath("cura"))) copy(self, "*", cura_private_data.resdirs[0], str(self._share_dir.joinpath("cura")))
if self.options.devtools: if self.options.devtools:
entitlements_file = "'{}'".format(os.path.join(self.source_folder, "packaging", "MacOS", "cura.entitlements"))
self._generate_pyinstaller_spec(location = self.generators_folder,
entrypoint_location = "'{}'".format(os.path.join(self.source_folder, self.conan_data["pyinstaller"]["runinfo"]["entrypoint"])).replace("\\", "\\\\"),
icon_path = "'{}'".format(os.path.join(self.source_folder, "packaging", self.conan_data["pyinstaller"]["icon"][str(self.settings.os)])).replace("\\", "\\\\"),
entitlements_file = entitlements_file if self.settings.os == "Macos" else "None")
# Update the po and pot files # Update the po and pot files
if self.settings.os != "Windows" or self.conf.get("tools.microsoft.bash:path", check_type=str): if self.settings.os != "Windows" or self.conf.get("tools.microsoft.bash:path", check_type=str):
vb = VirtualBuildEnv(self) vb = VirtualBuildEnv(self)
@ -451,13 +363,6 @@ echo "CURA_APP_NAME={{ cura_app_name }}" >> ${{ env_prefix }}GITHUB_ENV
save(self, os.path.join(self._script_dir, f"activate_github_actions_version_env{ext}"), activate_github_actions_version_env) save(self, os.path.join(self._script_dir, f"activate_github_actions_version_env{ext}"), activate_github_actions_version_env)
self._generate_cura_version(os.path.join(self._site_packages, "cura")) self._generate_cura_version(os.path.join(self._site_packages, "cura"))
self._generate_about_versions(str(self._share_dir.joinpath("cura", "resources", "qml", "Dialogs")))
entitlements_file = "'{}'".format(Path(self.cpp_info.res_paths[2], "MacOS", "cura.entitlements"))
self._generate_pyinstaller_spec(location = self._base_dir,
entrypoint_location = "'{}'".format(os.path.join(self.package_folder, self.cpp_info.bindirs[0], self.conan_data["pyinstaller"]["runinfo"]["entrypoint"])).replace("\\", "\\\\"),
icon_path = "'{}'".format(os.path.join(self.package_folder, self.cpp_info.resdirs[2], self.conan_data["pyinstaller"]["icon"][str(self.settings.os)])).replace("\\", "\\\\"),
entitlements_file = entitlements_file if self.settings.os == "Macos" else "None")
def package(self): def package(self):
copy(self, "cura_app.py", src = self.source_folder, dst = os.path.join(self.package_folder, self.cpp.package.bindirs[0])) copy(self, "cura_app.py", src = self.source_folder, dst = os.path.join(self.package_folder, self.cpp.package.bindirs[0]))

View file

@ -1,4 +1,4 @@
# Copyright (c) 2022 UltiMaker # Copyright (c) 2023 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
# --------- # ---------
@ -69,13 +69,25 @@ try:
except ImportError: except ImportError:
CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME
DEPENDENCY_INFO = {}
try: try:
from pathlib import Path from cura.CuraVersion import ConanInstalls
conan_install_info = Path(__file__).parent.parent.joinpath("conan_install_info.json")
if conan_install_info.exists(): if type(ConanInstalls) == dict:
import json CONAN_INSTALLS = ConanInstalls
with open(conan_install_info, "r") as f: else:
DEPENDENCY_INFO = json.loads(f.read()) CONAN_INSTALLS = {}
except:
pass except ImportError:
CONAN_INSTALLS = {}
try:
from cura.CuraVersion import PythonInstalls
if type(PythonInstalls) == dict:
PYTHON_INSTALLS = PythonInstalls
else:
PYTHON_INSTALLS = {}
except ImportError:
PYTHON_INSTALLS = {}

View file

@ -269,6 +269,9 @@ class CuraApplication(QtApplication):
CentralFileStorage.setIsEnterprise(ApplicationMetadata.IsEnterpriseVersion) CentralFileStorage.setIsEnterprise(ApplicationMetadata.IsEnterpriseVersion)
Resources.setIsEnterprise(ApplicationMetadata.IsEnterpriseVersion) Resources.setIsEnterprise(ApplicationMetadata.IsEnterpriseVersion)
self._conan_installs = ApplicationMetadata.CONAN_INSTALLS
self._python_installs = ApplicationMetadata.PYTHON_INSTALLS
@pyqtProperty(str, constant=True) @pyqtProperty(str, constant=True)
def ultimakerCloudApiRootUrl(self) -> str: def ultimakerCloudApiRootUrl(self) -> str:
return UltimakerCloudConstants.CuraCloudAPIRoot return UltimakerCloudConstants.CuraCloudAPIRoot
@ -851,11 +854,8 @@ class CuraApplication(QtApplication):
self._log_hardware_info() self._log_hardware_info()
if len(ApplicationMetadata.DEPENDENCY_INFO) > 0: Logger.debug("Using conan dependencies: {}", str(self.conanInstalls))
Logger.debug("Using Conan managed dependencies: " + ", ".join( Logger.debug("Using python dependencies: {}", str(self.pythonInstalls))
[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)
@ -2130,3 +2130,11 @@ class CuraApplication(QtApplication):
@pyqtProperty(bool, constant=True) @pyqtProperty(bool, constant=True)
def isEnterprise(self) -> bool: def isEnterprise(self) -> bool:
return ApplicationMetadata.IsEnterpriseVersion return ApplicationMetadata.IsEnterpriseVersion
@pyqtProperty("QVariant", constant=True)
def conanInstalls(self) -> Dict[str, Dict[str, str]]:
return self._conan_installs
@pyqtProperty("QVariant", constant=True)
def pythonInstalls(self) -> Dict[str, Dict[str, str]]:
return self._python_installs

View file

@ -1,19 +1,22 @@
// Copyright (c) 2022 UltiMaker // Copyright (c) 2023 UltiMaker
// Cura is released under the terms of the LGPLv3 or higher. // Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.4 import QtQuick 2.4
import QtQuick.Controls 2.9 import QtQuick.Controls 2.9
import QtQuick.Layouts 1.3
import UM 1.6 as UM import UM 1.6 as UM
import Cura 1.5 as Cura import Cura 1.6 as Cura
UM.Dialog UM.Dialog
{ {
id: base id: base
//: About dialog title
title: catalog.i18nc("@title:window The argument is the application name.", "About %1").arg(CuraApplication.applicationDisplayName) title: catalog.i18nc("@title:window The argument is the application name.", "About %1").arg(CuraApplication.applicationDisplayName)
// Flag to toggle between main dependencies information and extensive dependencies information
property bool showDefaultDependencies: true
minimumWidth: 500 * screenScaleFactor minimumWidth: 500 * screenScaleFactor
minimumHeight: 700 * screenScaleFactor minimumHeight: 700 * screenScaleFactor
width: minimumWidth width: minimumWidth
@ -21,186 +24,241 @@ UM.Dialog
backgroundColor: UM.Theme.getColor("main_background") backgroundColor: UM.Theme.getColor("main_background")
headerComponent: Rectangle
Rectangle
{ {
id: header width: parent.width
width: parent.width + 2 * margin // margin from Dialog.qml height: logo.height + 2 * UM.Theme.getSize("wide_margin").height
height: childrenRect.height + topPadding
anchors.top: parent.top
anchors.topMargin: -margin
anchors.horizontalCenter: parent.horizontalCenter
property real topPadding: UM.Theme.getSize("wide_margin").height
color: UM.Theme.getColor("main_window_header_background") color: UM.Theme.getColor("main_window_header_background")
Image Image
{ {
id: logo id: logo
width: (base.minimumWidth * 0.85) | 0 width: Math.floor(base.width * 0.85)
height: (width * (UM.Theme.getSize("logo").height / UM.Theme.getSize("logo").width)) | 0 height: Math.floor(width * UM.Theme.getSize("logo").height / UM.Theme.getSize("logo").width)
source: UM.Theme.getImage("logo") source: UM.Theme.getImage("logo")
sourceSize.width: width
sourceSize.height: height
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
anchors.top: parent.top anchors.centerIn: parent
anchors.topMargin: parent.topPadding
anchors.horizontalCenter: parent.horizontalCenter
UM.I18nCatalog{id: catalog; name: "cura"} UM.I18nCatalog{ id: catalog; name: "cura" }
MouseArea
{
anchors.fill: parent
onClicked:
{
projectsList.visible = !projectsList.visible;
projectBuildInfoList.visible = !projectBuildInfoList.visible;
}
}
} }
UM.Label UM.Label
{ {
id: version id: version
text: catalog.i18nc("@label","version: %1").arg(UM.Application.version) text: catalog.i18nc("@label","version: %1").arg(UM.Application.version)
font: UM.Theme.getFont("large_bold") font: UM.Theme.getFont("large_bold")
color: UM.Theme.getColor("button_text") color: UM.Theme.getColor("button_text")
anchors.right : logo.right anchors.right : logo.right
anchors.top: logo.bottom anchors.top: logo.bottom
anchors.topMargin: (UM.Theme.getSize("default_margin").height / 2) | 0 }
MouseArea
{
anchors.fill: parent
onDoubleClicked: showDefaultDependencies = !showDefaultDependencies
} }
} }
UM.Label // Reusable component to display a dependency
readonly property Component dependency_row: RowLayout
{ {
id: description spacing: UM.Theme.getSize("narrow_margin").width
width: parent.width
//: About dialog application description UM.Label
text: catalog.i18nc("@label","End-to-end solution for fused filament 3D printing.")
font: UM.Theme.getFont("system")
wrapMode: Text.WordWrap
anchors.top: header.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
}
UM.Label
{
id: creditsNotes
width: parent.width
//: About dialog application author note
text: catalog.i18nc("@info:credit","Cura is developed by UltiMaker in cooperation with the community.\nCura proudly uses the following open source projects:")
font: UM.Theme.getFont("system")
wrapMode: Text.WordWrap
anchors.top: description.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
}
ListView
{
id: projectsList
anchors.top: creditsNotes.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width
height: base.height - y - (2 * UM.Theme.getSize("default_margin").height + closeButton.height)
ScrollBar.vertical: UM.ScrollBar
{ {
id: projectsListScrollBar text: {
if (typeof(url) !== "undefined" && url !== "") {
return "<a href=\"" + url + "\">" + name + "</a>";
} else {
return name;
}
}
visible: text !== ""
Layout.fillWidth: true
Layout.preferredWidth: 2
elide: Text.ElideRight
} }
delegate: Row UM.Label
{ {
text: description
visible: text !== ""
Layout.fillWidth: true
Layout.preferredWidth: 3
elide: Text.ElideRight
}
UM.Label
{
text: license
visible: text !== ""
Layout.fillWidth: true
Layout.preferredWidth: 2
elide: Text.ElideRight
}
UM.Label
{
text: version
visible: text !== ""
Layout.fillWidth: true
Layout.preferredWidth: 2
elide: Text.ElideRight
}
}
Flickable
{
anchors.fill: parent
ScrollBar.vertical: UM.ScrollBar {
visible: contentHeight > height
}
contentHeight: content.height
clip: true
Column
{
id: content
spacing: UM.Theme.getSize("narrow_margin").width spacing: UM.Theme.getSize("narrow_margin").width
width: parent.width
UM.Label UM.Label
{ {
text: "<a href='%1' title='%2'>%2</a>".arg(model.url).arg(model.name) text: catalog.i18nc("@label", "End-to-end solution for fused filament 3D printing.")
width: (projectsList.width * 0.25) | 0 font: UM.Theme.getFont("system")
elide: Text.ElideRight wrapMode: Text.WordWrap
onLinkActivated: Qt.openUrlExternally(link)
} }
UM.Label UM.Label
{ {
text: model.description text: catalog.i18nc("@info:credit", "Cura is developed by UltiMaker in cooperation with the community.\nCura proudly uses the following open source projects:")
elide: Text.ElideRight font: UM.Theme.getFont("system")
width: ((projectsList.width * 0.6) | 0) - parent.spacing * 2 - projectsListScrollBar.width wrapMode: Text.WordWrap
} }
Column
{
visible: showDefaultDependencies
width: parent.width
Repeater
{
width: parent.width
delegate: Loader {
sourceComponent: dependency_row
width: parent.width
property string name: model.name
property string description: model.description
property string license: model.license
property string url: model.url
}
model: ListModel
{
id: projectsModel
}
Component.onCompleted:
{
//Do NOT add dependencies of our dependencies here, nor CI-dependencies!
//UltiMaker's own projects and forks.
projectsModel.append({ name: "Cura", description: catalog.i18nc("@label Description for application component", "Graphical user interface"), license: "LGPLv3", url: "https://github.com/Ultimaker/Cura" });
projectsModel.append({ name: "Uranium", description: catalog.i18nc("@label Description for application component", "Application framework"), license: "LGPLv3", url: "https://github.com/Ultimaker/Uranium" });
projectsModel.append({ name: "CuraEngine", description: catalog.i18nc("@label Description for application component", "G-code generator"), license: "AGPLv3", url: "https://github.com/Ultimaker/CuraEngine" });
projectsModel.append({ name: "libArcus", description: catalog.i18nc("@label Description for application component", "Interprocess communication library"), license: "LGPLv3", url: "https://github.com/Ultimaker/libArcus" });
projectsModel.append({ name: "pynest2d", description: catalog.i18nc("@label Description for application component", "Python bindings for libnest2d"), license: "LGPL", url: "https://github.com/Ultimaker/pynest2d" });
projectsModel.append({ name: "libnest2d", description: catalog.i18nc("@label Description for application component", "Polygon packing library, developed by Prusa Research"), license: "LGPL", url: "https://github.com/tamasmeszaros/libnest2d" });
projectsModel.append({ name: "libSavitar", description: catalog.i18nc("@label Description for application component", "Support library for handling 3MF files"), license: "LGPLv3", url: "https://github.com/ultimaker/libsavitar" });
projectsModel.append({ name: "libCharon", description: catalog.i18nc("@label Description for application component", "Support library for file metadata and streaming"), license: "LGPLv3", url: "https://github.com/ultimaker/libcharon" });
//Direct dependencies of the front-end.
projectsModel.append({ name: "Python", description: catalog.i18nc("@label Description for application dependency", "Programming language"), license: "Python", url: "http://python.org/" });
projectsModel.append({ name: "Qt6", description: catalog.i18nc("@label Description for application dependency", "GUI framework"), license: "LGPLv3", url: "https://www.qt.io/" });
projectsModel.append({ name: "PyQt", description: catalog.i18nc("@label Description for application dependency", "GUI framework bindings"), license: "GPL", url: "https://riverbankcomputing.com/software/pyqt" });
projectsModel.append({ name: "SIP", description: catalog.i18nc("@label Description for application dependency", "C/C++ Binding library"), license: "GPL", url: "https://riverbankcomputing.com/software/sip" });
projectsModel.append({ name: "Protobuf", description: catalog.i18nc("@label Description for application dependency", "Data interchange format"), license: "BSD", url: "https://developers.google.com/protocol-buffers" });
projectsModel.append({ name: "Noto Sans", description: catalog.i18nc("@label", "Font"), license: "Apache 2.0", url: "https://www.google.com/get/noto/" });
//CuraEngine's dependencies.
projectsModel.append({ name: "Clipper", description: catalog.i18nc("@label Description for application dependency", "Polygon clipping library"), license: "Boost", url: "http://www.angusj.com/delphi/clipper.php" });
projectsModel.append({ name: "RapidJSON", description: catalog.i18nc("@label Description for application dependency", "JSON parser"), license: "MIT", url: "https://rapidjson.org/" });
projectsModel.append({ name: "STB", description: catalog.i18nc("@label Description for application dependency", "Utility functions, including an image loader"), license: "Public Domain", url: "https://github.com/nothings/stb" });
projectsModel.append({ name: "Boost", description: catalog.i18nc("@label Description for application dependency", "Utility library, including Voronoi generation"), license: "Boost", url: "https://www.boost.org/" });
//Python modules.
projectsModel.append({ name: "Certifi", description: catalog.i18nc("@label Description for application dependency", "Root Certificates for validating SSL trustworthiness"), license: "MPL", url: "https://github.com/certifi/python-certifi" });
projectsModel.append({ name: "Cryptography", description: catalog.i18nc("@label Description for application dependency", "Root Certificates for validating SSL trustworthiness"), license: "APACHE and BSD", url: "https://cryptography.io/" });
projectsModel.append({ name: "Future", description: catalog.i18nc("@label Description for application dependency", "Compatibility between Python 2 and 3"), license: "MIT", url: "https://python-future.org/" });
projectsModel.append({ name: "keyring", description: catalog.i18nc("@label Description for application dependency", "Support library for system keyring access"), license: "MIT", url: "https://github.com/jaraco/keyring" });
projectsModel.append({ name: "NumPy", description: catalog.i18nc("@label Description for application dependency", "Support library for faster math"), license: "BSD", url: "http://www.numpy.org/" });
projectsModel.append({ name: "NumPy-STL", description: catalog.i18nc("@label Description for application dependency", "Support library for handling STL files"), license: "BSD", url: "https://github.com/WoLpH/numpy-stl" });
projectsModel.append({ name: "PyClipper", description: catalog.i18nc("@label Description for application dependency", "Python bindings for Clipper"), license: "MIT", url: "https://github.com/fonttools/pyclipper" });
projectsModel.append({ name: "PySerial", description: catalog.i18nc("@label Description for application dependency", "Serial communication library"), license: "Python", url: "http://pyserial.sourceforge.net/" });
projectsModel.append({ name: "SciPy", description: catalog.i18nc("@label Description for application dependency", "Support library for scientific computing"), license: "BSD-new", url: "https://www.scipy.org/" });
projectsModel.append({ name: "Sentry", description: catalog.i18nc("@Label Description for application dependency", "Python Error tracking library"), license: "BSD 2-Clause 'Simplified'", url: "https://sentry.io/for/python/" });
projectsModel.append({ name: "Trimesh", description: catalog.i18nc("@label Description for application dependency", "Support library for handling triangular meshes"), license: "MIT", url: "https://trimsh.org" });
projectsModel.append({ name: "python-zeroconf", description: catalog.i18nc("@label Description for application dependency", "ZeroConf discovery library"), license: "LGPL", url: "https://github.com/jstasiak/python-zeroconf" });
//Building/packaging.
projectsModel.append({ name: "CMake", description: catalog.i18nc("@label Description for development tool", "Universal build system configuration"), license: "BSD 3-Clause", url: "https://cmake.org/" });
projectsModel.append({ name: "Conan", description: catalog.i18nc("@label Description for development tool", "Dependency and package manager"), license: "MIT", url: "https://conan.io/" });
projectsModel.append({ name: "Pyinstaller", description: catalog.i18nc("@label Description for development tool", "Packaging Python-applications"), license: "GPLv2", url: "https://pyinstaller.org/" });
projectsModel.append({ name: "AppImageKit", description: catalog.i18nc("@label Description for development tool", "Linux cross-distribution application deployment"), license: "MIT", url: "https://github.com/AppImage/AppImageKit" });
projectsModel.append({ name: "NSIS", description: catalog.i18nc("@label Description for development tool", "Generating Windows installers"), license: "Zlib", url: "https://nsis.sourceforge.io/" });
}
}
}
UM.Label UM.Label
{ {
text: model.license visible: !showDefaultDependencies
elide: Text.ElideRight text: "Conan Installs"
width: (projectsList.width * 0.15) | 0 font: UM.Theme.getFont("large_bold")
}
Column
{
visible: !showDefaultDependencies
width: parent.width
Repeater
{
width: parent.width
model: Object.entries(CuraApplication.conanInstalls).map(function (item) { return { name: item[0], version: item[1].version } })
delegate: Loader {
sourceComponent: dependency_row
width: parent.width
property string name: modelData.name
property string version: modelData.version
}
}
}
UM.Label
{
visible: !showDefaultDependencies
text: "Python Installs"
font: UM.Theme.getFont("large_bold")
}
Column
{
width: parent.width
visible: !showDefaultDependencies
Repeater
{
delegate: Loader {
sourceComponent: dependency_row
width: parent.width
property string name: modelData.name
property string version: modelData.version
}
width: parent.width
model: Object.entries(CuraApplication.pythonInstalls).map(function (item) { return { name: item[0], version: item[1].version } })
}
} }
} }
model: ListModel
{
id: projectsModel
}
Component.onCompleted:
{
//Do NOT add dependencies of our dependencies here, nor CI-dependencies!
//UltiMaker's own projects and forks.
projectsModel.append({ name: "Cura", description: catalog.i18nc("@label Description for application component", "Graphical user interface"), license: "LGPLv3", url: "https://github.com/Ultimaker/Cura" });
projectsModel.append({ name: "Uranium", description: catalog.i18nc("@label Description for application component", "Application framework"), license: "LGPLv3", url: "https://github.com/Ultimaker/Uranium" });
projectsModel.append({ name: "CuraEngine", description: catalog.i18nc("@label Description for application component", "G-code generator"), license: "AGPLv3", url: "https://github.com/Ultimaker/CuraEngine" });
projectsModel.append({ name: "libArcus", description: catalog.i18nc("@label Description for application component", "Interprocess communication library"), license: "LGPLv3", url: "https://github.com/Ultimaker/libArcus" });
projectsModel.append({ name: "pynest2d", description: catalog.i18nc("@label Description for application component", "Python bindings for libnest2d"), license: "LGPL", url: "https://github.com/Ultimaker/pynest2d" });
projectsModel.append({ name: "libnest2d", description: catalog.i18nc("@label Description for application component", "Polygon packing library, developed by Prusa Research"), license: "LGPL", url: "https://github.com/tamasmeszaros/libnest2d" });
projectsModel.append({ name: "libSavitar", description: catalog.i18nc("@label Description for application component", "Support library for handling 3MF files"), license: "LGPLv3", url: "https://github.com/ultimaker/libsavitar" });
projectsModel.append({ name: "libCharon", description: catalog.i18nc("@label Description for application component", "Support library for file metadata and streaming"), license: "LGPLv3", url: "https://github.com/ultimaker/libcharon" });
//Direct dependencies of the front-end.
projectsModel.append({ name: "Python", description: catalog.i18nc("@label Description for application dependency", "Programming language"), license: "Python", url: "http://python.org/" });
projectsModel.append({ name: "Qt6", description: catalog.i18nc("@label Description for application dependency", "GUI framework"), license: "LGPLv3", url: "https://www.qt.io/" });
projectsModel.append({ name: "PyQt", description: catalog.i18nc("@label Description for application dependency", "GUI framework bindings"), license: "GPL", url: "https://riverbankcomputing.com/software/pyqt" });
projectsModel.append({ name: "SIP", description: catalog.i18nc("@label Description for application dependency", "C/C++ Binding library"), license: "GPL", url: "https://riverbankcomputing.com/software/sip" });
projectsModel.append({ name: "Protobuf", description: catalog.i18nc("@label Description for application dependency", "Data interchange format"), license: "BSD", url: "https://developers.google.com/protocol-buffers" });
projectsModel.append({ name: "Noto Sans", description: catalog.i18nc("@label", "Font"), license: "Apache 2.0", url: "https://www.google.com/get/noto/" });
//CuraEngine's dependencies.
projectsModel.append({ name: "Clipper", description: catalog.i18nc("@label Description for application dependency", "Polygon clipping library"), license: "Boost", url: "http://www.angusj.com/delphi/clipper.php" });
projectsModel.append({ name: "RapidJSON", description: catalog.i18nc("@label Description for application dependency", "JSON parser"), license: "MIT", url: "https://rapidjson.org/" });
projectsModel.append({ name: "STB", description: catalog.i18nc("@label Description for application dependency", "Utility functions, including an image loader"), license: "Public Domain", url: "https://github.com/nothings/stb" });
projectsModel.append({ name: "Boost", description: catalog.i18nc("@label Description for application dependency", "Utility library, including Voronoi generation"), license: "Boost", url: "https://www.boost.org/" });
//Python modules.
projectsModel.append({ name: "Certifi", description: catalog.i18nc("@label Description for application dependency", "Root Certificates for validating SSL trustworthiness"), license: "MPL", url: "https://github.com/certifi/python-certifi" });
projectsModel.append({ name: "Cryptography", description: catalog.i18nc("@label Description for application dependency", "Root Certificates for validating SSL trustworthiness"), license: "APACHE and BSD", url: "https://cryptography.io/" });
projectsModel.append({ name: "Future", description: catalog.i18nc("@label Description for application dependency", "Compatibility between Python 2 and 3"), license: "MIT", url: "https://python-future.org/" });
projectsModel.append({ name: "keyring", description: catalog.i18nc("@label Description for application dependency", "Support library for system keyring access"), license: "MIT", url: "https://github.com/jaraco/keyring" });
projectsModel.append({ name: "NumPy", description: catalog.i18nc("@label Description for application dependency", "Support library for faster math"), license: "BSD", url: "http://www.numpy.org/" });
projectsModel.append({ name: "NumPy-STL", description: catalog.i18nc("@label Description for application dependency", "Support library for handling STL files"), license: "BSD", url: "https://github.com/WoLpH/numpy-stl" });
projectsModel.append({ name: "PyClipper", description: catalog.i18nc("@label Description for application dependency", "Python bindings for Clipper"), license: "MIT", url: "https://github.com/fonttools/pyclipper" });
projectsModel.append({ name: "PySerial", description: catalog.i18nc("@label Description for application dependency", "Serial communication library"), license: "Python", url: "http://pyserial.sourceforge.net/" });
projectsModel.append({ name: "SciPy", description: catalog.i18nc("@label Description for application dependency", "Support library for scientific computing"), license: "BSD-new", url: "https://www.scipy.org/" });
projectsModel.append({ name: "Sentry", description: catalog.i18nc("@Label Description for application dependency", "Python Error tracking library"), license: "BSD 2-Clause 'Simplified'", url: "https://sentry.io/for/python/" });
projectsModel.append({ name: "Trimesh", description: catalog.i18nc("@label Description for application dependency", "Support library for handling triangular meshes"), license: "MIT", url: "https://trimsh.org" });
projectsModel.append({ name: "python-zeroconf", description: catalog.i18nc("@label Description for application dependency", "ZeroConf discovery library"), license: "LGPL", url: "https://github.com/jstasiak/python-zeroconf" });
//Building/packaging.
projectsModel.append({ name: "CMake", description: catalog.i18nc("@label Description for development tool", "Universal build system configuration"), license: "BSD 3-Clause", url: "https://cmake.org/" });
projectsModel.append({ name: "Conan", description: catalog.i18nc("@label Description for development tool", "Dependency and package manager"), license: "MIT", url: "https://conan.io/" });
projectsModel.append({ name: "Pyinstaller", description: catalog.i18nc("@label Description for development tool", "Packaging Python-applications"), license: "GPLv2", url: "https://pyinstaller.org/" });
projectsModel.append({ name: "AppImageKit", description: catalog.i18nc("@label Description for development tool", "Linux cross-distribution application deployment"), license: "MIT", url: "https://github.com/AppImage/AppImageKit" });
projectsModel.append({ name: "NSIS", description: catalog.i18nc("@label Description for development tool", "Generating Windows installers"), license: "Zlib", url: "https://nsis.sourceforge.io/" });
}
}
AboutDialogVersionsList{
id: projectBuildInfoList
}
onVisibleChanged:
{
projectsList.visible = true;
projectBuildInfoList.visible = false;
} }
rightButtons: Cura.TertiaryButton rightButtons: Cura.TertiaryButton