Merge branch 'master' into libArachne_rebased

This commit is contained in:
Remco Burema 2021-07-16 08:49:46 +02:00
commit 6c08bbfc9d
No known key found for this signature in database
GPG key ID: 215C49431D43F98C
122 changed files with 2907 additions and 524 deletions

View file

@ -4,12 +4,12 @@
import argparse #To run the engine in debug mode if the front-end is in debug mode.
from collections import defaultdict
import os
from PyQt5.QtCore import QObject, QTimer, pyqtSlot
from PyQt5.QtCore import QObject, QTimer, QUrl, pyqtSlot
import sys
from time import time
from typing import Any, cast, Dict, List, Optional, Set, TYPE_CHECKING
from PyQt5.QtGui import QImage
from PyQt5.QtGui import QDesktopServices, QImage
from UM.Backend.Backend import Backend, BackendState
from UM.Scene.SceneNode import SceneNode
@ -157,6 +157,18 @@ class CuraEngineBackend(QObject, Backend):
self.determineAutoSlicing()
application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
self._slicing_error_message = Message(
text = catalog.i18nc("@message", "Slicing failed with an unexpected error. Please consider reporting a bug on our issue tracker."),
title = catalog.i18nc("@message:title", "Slicing failed")
)
self._slicing_error_message.addAction(
action_id = "report_bug",
name = catalog.i18nc("@message:button", "Report a bug"),
description = catalog.i18nc("@message:description", "Report a bug on Ultimaker Cura's issue tracker."),
icon = "[no_icon]"
)
self._slicing_error_message.actionTriggered.connect(self._reportBackendError)
self._snapshot = None #type: Optional[QImage]
application.initializationFinished.connect(self.initialize)
@ -598,10 +610,15 @@ class CuraEngineBackend(QObject, Backend):
if error.getErrorCode() not in [Arcus.ErrorCode.BindFailedError, Arcus.ErrorCode.ConnectionResetError, Arcus.ErrorCode.Debug]:
Logger.log("w", "A socket error caused the connection to be reset")
elif error.getErrorCode() == Arcus.ErrorCode.ConnectionResetError:
Logger.error("CuraEngine crashed abnormally! The socket connection was reset unexpectedly.")
self._slicing_error_message.show()
self.setState(BackendState.Error)
self.stopSlicing()
# _terminate()' function sets the job status to 'cancel', after reconnecting to another Port the job status
# needs to be updated. Otherwise backendState is "Unable To Slice"
if error.getErrorCode() == Arcus.ErrorCode.BindFailedError and self._start_slice_job is not None:
elif error.getErrorCode() == Arcus.ErrorCode.BindFailedError and self._start_slice_job is not None:
self._start_slice_job.setIsCancelled(False)
# Check if there's any slicable object in the scene.
@ -922,9 +939,22 @@ class CuraEngineBackend(QObject, Backend):
if not self._restart:
if self._process: # type: ignore
Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait()) # type: ignore
return_code = self._process.wait()
if return_code != 0:
Logger.log("e", f"Backend exited abnormally with return code {return_code}!")
self._slicing_error_message.show()
self.setState(BackendState.Error)
self.stopSlicing()
else:
Logger.log("d", "Backend finished slicing. Resetting process and socket.")
self._process = None # type: ignore
def _reportBackendError(self, _message_id: str, _action_id: str) -> None:
"""
Triggered when the user wants to report an error in the back-end.
"""
QDesktopServices.openUrl(QUrl("https://github.com/Ultimaker/Cura/issues/new/choose"))
def _onGlobalStackChanged(self) -> None:
"""Called when the global container stack changes"""

View file

@ -0,0 +1,62 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 378.13 348.13">
<defs>
<style>
.cls-2,.cls-6{fill:#c5dbfb;}
.cls-3,.cls-5,.cls-8{fill:#fff;}
.cls-3{stroke:#c5dbfb;}
.cls-10,.cls-3,.cls-4,.cls-6,.cls-7,.cls-8{stroke-miterlimit:10;stroke-width:2px;}
.cls-4,.cls-7{fill:#f3f8fe;}
.cls-4{stroke:#f3f8fe;}
.cls-6,.cls-7,.cls-8{stroke:#061884;}
.cls-10{fill:#196ef0;stroke:#196ef0;}
.cls-11{fill:#061884;}
</style>
<clipPath id="clip-path">
<circle fill="none" cx="155" cy="125" r="80" />
</clipPath>
</defs>
<path class="cls-2" d="M43,17V3H83a2,2,0,0,1,2,2V17Z" />
<path class="cls-3" d="M3,1H40L56,17H87a2,2,0,0,1,2,2V67a2,2,0,0,1-2,2H3a2,2,0,0,1-2-2V3A2,2,0,0,1,3,1Z" />
<path class="cls-2" d="M153,17V3h40a2,2,0,0,1,2,2V17Z" />
<path class="cls-3" d="M113,1h37l16,16h31a2,2,0,0,1,2,2V67a2,2,0,0,1-2,2H113a2,2,0,0,1-2-2V3A2,2,0,0,1,113,1Z" />
<path class="cls-2" d="M263,17V3h40a2,2,0,0,1,2,2V17Z" />
<path class="cls-3" d="M223,1h37l16,16h31a2,2,0,0,1,2,2V67a2,2,0,0,1-2,2H223a2,2,0,0,1-2-2V3A2,2,0,0,1,223,1Z" />
<path class="cls-2" d="M43,107V93H83a2,2,0,0,1,2,2v12Z" />
<path class="cls-3" d="M3,91H40l16,16H87a2,2,0,0,1,2,2v48a2,2,0,0,1-2,2H3a2,2,0,0,1-2-2V93A2,2,0,0,1,3,91Z" />
<path class="cls-4" d="M153,107V93h40a2,2,0,0,1,2,2v12Z" />
<path class="cls-3" d="M113,91h37l16,16h31a2,2,0,0,1,2,2v48a2,2,0,0,1-2,2H113a2,2,0,0,1-2-2V93A2,2,0,0,1,113,91Z" />
<path class="cls-2" d="M263,107V93h40a2,2,0,0,1,2,2v12Z" />
<path class="cls-3" d="M223,91h37l16,16h31a2,2,0,0,1,2,2v48a2,2,0,0,1-2,2H223a2,2,0,0,1-2-2V93A2,2,0,0,1,223,91Z" />
<path class="cls-2" d="M43,197V183H83a2,2,0,0,1,2,2v12Z" />
<path class="cls-3" d="M3,181H40l16,16H87a2,2,0,0,1,2,2v48a2,2,0,0,1-2,2H3a2,2,0,0,1-2-2V183A2,2,0,0,1,3,181Z" />
<path class="cls-4" d="M153,197V183h40a2,2,0,0,1,2,2v12Z" />
<path class="cls-3" d="M113,181h37l16,16h31a2,2,0,0,1,2,2v48a2,2,0,0,1-2,2H113a2,2,0,0,1-2-2V183A2,2,0,0,1,113,181Z" />
<path class="cls-2" d="M263,197V183h40a2,2,0,0,1,2,2v12Z" />
<path class="cls-3" d="M223,181h37l16,16h31a2,2,0,0,1,2,2v48a2,2,0,0,1-2,2H223a2,2,0,0,1-2-2V183A2,2,0,0,1,223,181Z" />
<circle class="cls-5" cx="155" cy="125" r="100" />
<path class="cls-6" d="M351.12,322.62h20a10,10,0,0,1,10,10v7a0,0,0,0,1,0,0h-40a0,0,0,0,1,0,0v-7A10,10,0,0,1,351.12,322.62Z" transform="translate(850.61 309.91) rotate(135)" />
<rect class="cls-7" x="293.75" y="225.25" width="40" height="117" transform="translate(-108.74 304.96) rotate(-45)" />
<polyline class="cls-7" points="213.69 199.25 252.58 238.14 267.43 223.29 228.54 184.4" />
<circle class="cls-8" cx="155" cy="125" r="95" />
<circle class="cls-8" cx="155" cy="125" r="85" />
<path class="cls-6" d="M256.37,227.87h20a10,10,0,0,1,10,10v7a0,0,0,0,1,0,0h-40a0,0,0,0,1,0,0v-7a10,10,0,0,1,10-10Z" transform="translate(-89.12 257.58) rotate(-45)" />
<g clip-path="url(#clip-path)">
<path class="cls-10" d="M43,17V3H83a2,2,0,0,1,2,2V17Z" />
<path class="cls-8" d="M3,1H40L56,17H87a2,2,0,0,1,2,2V67a2,2,0,0,1-2,2H3a2,2,0,0,1-2-2V3A2,2,0,0,1,3,1Z" />
<path class="cls-10" d="M153,17V3h40a2,2,0,0,1,2,2V17Z" />
<path class="cls-8" d="M113,1h37l16,16h31a2,2,0,0,1,2,2V67a2,2,0,0,1-2,2H113a2,2,0,0,1-2-2V3A2,2,0,0,1,113,1Z" />
<path class="cls-10" d="M263,17V3h40a2,2,0,0,1,2,2V17Z" />
<path class="cls-8" d="M223,1h37l16,16h31a2,2,0,0,1,2,2V67a2,2,0,0,1-2,2H223a2,2,0,0,1-2-2V3A2,2,0,0,1,223,1Z" />
<path class="cls-10" d="M43,107V93H83a2,2,0,0,1,2,2v12Z" />
<path class="cls-8" d="M3,91H40l16,16H87a2,2,0,0,1,2,2v48a2,2,0,0,1-2,2H3a2,2,0,0,1-2-2V93A2,2,0,0,1,3,91Z" />
<path class="cls-10" d="M263,107V93h40a2,2,0,0,1,2,2v12Z" />
<path class="cls-8" d="M223,91h37l16,16h31a2,2,0,0,1,2,2v48a2,2,0,0,1-2,2H223a2,2,0,0,1-2-2V93A2,2,0,0,1,223,91Z" />
<path class="cls-10" d="M43,197V183H83a2,2,0,0,1,2,2v12Z" />
<path class="cls-8" d="M3,181H40l16,16H87a2,2,0,0,1,2,2v48a2,2,0,0,1-2,2H3a2,2,0,0,1-2-2V183A2,2,0,0,1,3,181Z" />
<path class="cls-10" d="M153,197V183h40a2,2,0,0,1,2,2v12Z" />
<path class="cls-8" d="M113,181h37l16,16h31a2,2,0,0,1,2,2v48a2,2,0,0,1-2,2H113a2,2,0,0,1-2-2V183A2,2,0,0,1,113,181Z" />
<path class="cls-10" d="M263,197V183h40a2,2,0,0,1,2,2v12Z" />
<path class="cls-8" d="M223,181h37l16,16h31a2,2,0,0,1,2,2v48a2,2,0,0,1-2,2H223a2,2,0,0,1-2-2V183A2,2,0,0,1,223,181Z" />
<path class="cls-11" d="M149.18,133.69v-3.48a14.36,14.36,0,0,1,1.74-7.25,20.17,20.17,0,0,1,6.4-6.17A25.87,25.87,0,0,0,163,112a7,7,0,0,0,1.48-4.34,4.13,4.13,0,0,0-1.93-3.62,9,9,0,0,0-5.14-1.3,24.94,24.94,0,0,0-7.34,1.16,45.2,45.2,0,0,0-7.78,3.31l-5.37-10.64a48.41,48.41,0,0,1,9.89-4.21,40.25,40.25,0,0,1,11.67-1.61q9.57,0,14.9,4.43a14.16,14.16,0,0,1,5.32,11.41,15.41,15.41,0,0,1-2.55,9,30.38,30.38,0,0,1-7.92,7.34A32.11,32.11,0,0,0,163,127.3a5.91,5.91,0,0,0-1.34,4v2.41Zm-1.61,15.12q0-4.38,2.46-6.12a10,10,0,0,1,5.95-1.75,9.69,9.69,0,0,1,5.77,1.75q2.46,1.74,2.46,6.12,0,4.22-2.46,6a9.42,9.42,0,0,1-5.77,1.84,9.69,9.69,0,0,1-5.95-1.84Q147.57,153,147.57,148.81Z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -1,10 +1,12 @@
// Copyright (C) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Window 2.2
import QtQuick.Controls 1.4 as OldControls // TableView doesn't exist in the QtQuick Controls 2.x in 5.10, so use the old one
import QtQuick.Controls 2.3
import QtQuick.Controls.Styles 1.4
import QtQuick.Layouts 1.1
import UM 1.2 as UM
import Cura 1.6 as Cura
@ -29,31 +31,43 @@ Item
margins: UM.Theme.getSize("default_margin").width
}
Label
RowLayout
{
id: selectProjectLabel
id: headerRow
text: "Select Project"
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("small_button_text")
anchors.top: parent.top
anchors.left: parent.left
visible: projectListContainer.visible
}
Cura.SecondaryButton
{
id: createNewProjectButton
anchors.verticalCenter: selectProjectLabel.verticalCenter
anchors.right: parent.right
text: "New Library project"
onClicked:
anchors
{
createNewProjectPopup.open()
top: parent.top
left: parent.left
right: parent.right
}
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
Cura.TextField
{
id: searchBar
Layout.fillWidth: true
implicitHeight: createNewProjectButton.height
onTextEdited: manager.projectFilter = text //Update the search filter when editing this text field.
leftIcon: UM.Theme.getIcon("Magnifier")
placeholderText: "Search"
}
Cura.SecondaryButton
{
id: createNewProjectButton
text: "New Library project"
onClicked:
{
createNewProjectPopup.open()
}
busy: manager.creatingNewProjectStatus == DF.RetrievalStatus.InProgress
}
busy: manager.creatingNewProjectStatus == DF.RetrievalStatus.InProgress
}
Item
@ -76,19 +90,18 @@ Item
{
id: digitalFactoryImage
anchors.horizontalCenter: parent.horizontalCenter
source: "../images/digital_factory.svg"
source: searchBar.text === "" ? "../images/digital_factory.svg" : "../images/projects_not_found.svg"
fillMode: Image.PreserveAspectFit
width: parent.width - 2 * UM.Theme.getSize("thick_margin").width
sourceSize.width: width
sourceSize.height: height
}
Label
{
id: noLibraryProjectsLabel
anchors.horizontalCenter: parent.horizontalCenter
text: "It appears that you don't have any projects in the Library yet."
text: searchBar.text === "" ? "It appears that you don't have any projects in the Library yet." : "No projects found that match the search query."
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
}
Cura.TertiaryButton
@ -97,6 +110,7 @@ Item
anchors.horizontalCenter: parent.horizontalCenter
text: "Visit Digital Library"
onClicked: Qt.openUrlExternally(CuraApplication.ultimakerDigitalFactoryUrl + "/app/library")
visible: searchBar.text === "" //Show the link to Digital Library when there are no projects in the user's Library.
}
}
}
@ -106,7 +120,7 @@ Item
id: projectListContainer
anchors
{
top: selectProjectLabel.bottom
top: headerRow.bottom
topMargin: UM.Theme.getSize("default_margin").height
bottom: parent.bottom
left: parent.left

View file

@ -22,6 +22,7 @@ from .DFFileUploader import DFFileUploader
from .DFLibraryFileUploadRequest import DFLibraryFileUploadRequest
from .DFLibraryFileUploadResponse import DFLibraryFileUploadResponse
from .DFPrintJobUploadRequest import DFPrintJobUploadRequest
from .DigitalFactoryFeatureBudgetResponse import DigitalFactoryFeatureBudgetResponse
from .DigitalFactoryFileResponse import DigitalFactoryFileResponse
from .DigitalFactoryProjectResponse import DigitalFactoryProjectResponse
from .PaginationLinks import PaginationLinks
@ -57,6 +58,27 @@ class DigitalFactoryApiClient:
self._projects_pagination_mgr = PaginationManager(limit = projects_limit_per_page) if projects_limit_per_page else None # type: Optional[PaginationManager]
def checkUserHasAccess(self, callback: Callable) -> None:
"""Checks if the user has any sort of access to the digital library.
A user is considered to have access if the max-# of private projects is greater then 0 (or -1 for unlimited).
"""
def callbackWrap(response: Optional[Any] = None, *args, **kwargs) -> None:
if (response is not None and isinstance(response, DigitalFactoryFeatureBudgetResponse) and
response.library_max_private_projects is not None):
callback(
response.library_max_private_projects == -1 or # Note: -1 is unlimited
response.library_max_private_projects > 0)
else:
Logger.warning(f"Digital Factory: Response is not a feature budget, likely an error: {str(response)}")
callback(False)
self._http.get(f"{self.CURA_API_ROOT}/feature_budgets",
scope = self._scope,
callback = self._parseCallback(callbackWrap, DigitalFactoryFeatureBudgetResponse, callbackWrap),
error_callback = callbackWrap,
timeout = self.DEFAULT_REQUEST_TIMEOUT)
def getProject(self, library_project_id: str, on_finished: Callable[[DigitalFactoryProjectResponse], Any], failed: Callable) -> None:
"""
Retrieves a digital factory project by its library project id.
@ -73,7 +95,7 @@ class DigitalFactoryApiClient:
error_callback = failed,
timeout = self.DEFAULT_REQUEST_TIMEOUT)
def getProjectsFirstPage(self, on_finished: Callable[[List[DigitalFactoryProjectResponse]], Any], failed: Callable) -> None:
def getProjectsFirstPage(self, search_filter: str, on_finished: Callable[[List[DigitalFactoryProjectResponse]], Any], failed: Callable) -> None:
"""
Retrieves digital factory projects for the user that is currently logged in.
@ -81,13 +103,18 @@ class DigitalFactoryApiClient:
according to the limit set in the pagination manager. If there is no projects pagination manager, this function
leaves the project limit to the default set on the server side (999999).
:param search_filter: Text to filter the search results. If given an empty string, results are not filtered.
:param on_finished: The function to be called after the result is parsed.
:param failed: The function to be called if the request fails.
"""
url = "{}/projects".format(self.CURA_API_ROOT)
url = f"{self.CURA_API_ROOT}/projects"
query_character = "?"
if self._projects_pagination_mgr:
self._projects_pagination_mgr.reset() # reset to clear all the links and response metadata
url += "?limit={}".format(self._projects_pagination_mgr.limit)
url += f"{query_character}limit={self._projects_pagination_mgr.limit}"
query_character = "&"
if search_filter != "":
url += f"{query_character}search={search_filter}"
self._http.get(url,
scope = self._scope,

View file

@ -1,4 +1,6 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import json
import math
import os
@ -8,7 +10,7 @@ from enum import IntEnum
from pathlib import Path
from typing import Optional, List, Dict, Any, cast
from PyQt5.QtCore import pyqtSignal, QObject, pyqtSlot, pyqtProperty, Q_ENUMS, QUrl
from PyQt5.QtCore import pyqtSignal, QObject, pyqtSlot, pyqtProperty, Q_ENUMS, QTimer, QUrl
from PyQt5.QtNetwork import QNetworkReply
from PyQt5.QtQml import qmlRegisterType, qmlRegisterUncreatableType
@ -89,6 +91,9 @@ class DigitalFactoryController(QObject):
uploadFileError = Signal()
uploadFileFinished = Signal()
"""Signal to inform about the state of user access."""
userAccessStateChanged = pyqtSignal(bool)
def __init__(self, application: CuraApplication) -> None:
super().__init__(parent = None)
@ -106,12 +111,18 @@ class DigitalFactoryController(QObject):
self._has_more_projects_to_load = False
self._account = self._application.getInstance().getCuraAPI().account # type: Account
self._account.loginStateChanged.connect(self._onLoginStateChanged)
self._current_workspace_information = CuraApplication.getInstance().getCurrentWorkspaceInformation()
# Initialize the project model
self._project_model = DigitalFactoryProjectModel()
self._selected_project_idx = -1
self._project_creation_error_text = "Something went wrong while creating a new project. Please try again."
self._project_filter = ""
self._project_filter_change_timer = QTimer()
self._project_filter_change_timer.setInterval(200)
self._project_filter_change_timer.setSingleShot(True)
self._project_filter_change_timer.timeout.connect(self._applyProjectFilter)
# Initialize the file model
self._file_model = DigitalFactoryFileModel()
@ -131,6 +142,8 @@ class DigitalFactoryController(QObject):
self._application.engineCreatedSignal.connect(self._onEngineCreated)
self._application.initializationFinished.connect(self._applicationInitializationFinished)
self._user_has_access = False
def clear(self) -> None:
self._project_model.clearProjects()
self._api.clear()
@ -143,16 +156,24 @@ class DigitalFactoryController(QObject):
self.setSelectedProjectIndex(-1)
def _onLoginStateChanged(self, logged_in: bool) -> None:
def callback(has_access, **kwargs):
self._user_has_access = has_access
self.userAccessStateChanged.emit(logged_in)
self._api.checkUserHasAccess(callback)
def userAccountHasLibraryAccess(self) -> bool:
"""
Checks whether the currently logged in user account has access to the Digital Library
:return: True if the user account has Digital Library access, else False
"""
subscriptions = [] # type: List[Dict[str, Any]]
if self._account.userProfile:
subscriptions = self._account.userProfile.get("subscriptions", [])
return len(subscriptions) > 0
if len(subscriptions) > 0:
return True
return self._user_has_access
def initialize(self, preselected_project_id: Optional[str] = None) -> None:
self.clear()
@ -162,7 +183,7 @@ class DigitalFactoryController(QObject):
if preselected_project_id:
self._api.getProject(preselected_project_id, on_finished = self.setProjectAsPreselected, failed = self._onGetProjectFailed)
else:
self._api.getProjectsFirstPage(on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed)
self._api.getProjectsFirstPage(search_filter = self._project_filter, on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed)
def setProjectAsPreselected(self, df_project: DigitalFactoryProjectResponse) -> None:
"""
@ -288,6 +309,38 @@ class DigitalFactoryController(QObject):
self._selected_file_indices = file_indices
self.selectedFileIndicesChanged.emit(file_indices)
def setProjectFilter(self, new_filter: str) -> None:
"""
Called when the user wants to change the search filter for projects.
The filter is not immediately applied. There is some delay to allow the user to finish typing.
:param new_filter: The new filter that the user wants to apply.
"""
self._project_filter = new_filter
self._project_filter_change_timer.start()
"""
Signal to notify Qt that the applied filter has changed.
"""
projectFilterChanged = pyqtSignal()
@pyqtProperty(str, notify = projectFilterChanged, fset = setProjectFilter)
def projectFilter(self) -> str:
"""
The current search filter being applied to the project list.
:return: The current search filter being applied to the project list.
"""
return self._project_filter
def _applyProjectFilter(self) -> None:
"""
Actually apply the current filter to search for projects with the user-defined search string.
:return:
"""
self.clear()
self.projectFilterChanged.emit()
self._api.getProjectsFirstPage(search_filter = self._project_filter, on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed)
@pyqtProperty(QObject, constant = True)
def digitalFactoryProjectModel(self) -> "DigitalFactoryProjectModel":
return self._project_model
@ -502,7 +555,7 @@ class DigitalFactoryController(QObject):
# false, we also need to clean it from the projects model
self._project_model.clearProjects()
self.setSelectedProjectIndex(-1)
self._api.getProjectsFirstPage(on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed)
self._api.getProjectsFirstPage(search_filter = self._project_filter, on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed)
self.setRetrievingProjectsStatus(RetrievalStatus.InProgress)
self._has_preselected_project = new_has_preselected_project
self.preselectedProjectChanged.emit()

View file

@ -0,0 +1,43 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from .BaseModel import BaseModel
from typing import Optional
class DigitalFactoryFeatureBudgetResponse(BaseModel):
"""Class representing the capabilities of a user account for Digital Library.
NOTE: For each max_..._projects fields, '-1' means unlimited!
"""
def __init__(self,
library_can_use_business_value: Optional[bool] = False,
library_can_use_comments: Optional[bool] = False,
library_can_use_status: Optional[bool] = False,
library_can_use_tags: Optional[bool] = False,
library_can_use_technical_requirements: Optional[bool] = False,
library_max_organization_shared_projects: Optional[int] = False, # -1 means unlimited
library_max_private_projects: Optional[int] = False, # -1 means unlimited
library_max_team_shared_projects: Optional[int] = False, # -1 means unlimited
**kwargs) -> None:
self.library_can_use_business_value = library_can_use_business_value
self.library_can_use_comments = library_can_use_comments
self.library_can_use_status = library_can_use_status
self.library_can_use_tags = library_can_use_tags
self.library_can_use_technical_requirements = library_can_use_technical_requirements
self.library_max_organization_shared_projects = library_max_organization_shared_projects # -1 means unlimited
self.library_max_private_projects = library_max_private_projects # -1 means unlimited
self.library_max_team_shared_projects = library_max_team_shared_projects # -1 means unlimited
super().__init__(**kwargs)
def __repr__(self) -> str:
return "max private: {}, max org: {}, max team: {}".format(
self.library_max_private_projects,
self.library_max_organization_shared_projects,
self.library_max_team_shared_projects)
# Validates the model, raising an exception if the model is invalid.
def validate(self) -> None:
super().validate()
# No validation for now, as the response can be "data: []", which should be interpreted as all False and 0's

View file

@ -22,7 +22,7 @@ class DigitalFactoryFileProvider(FileProvider):
self._dialog = None
self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account
self._account.loginStateChanged.connect(self._onLoginStateChanged)
self._controller.userAccessStateChanged.connect(self._onUserAccessStateChanged)
self.enabled = self._account.isLoggedIn and self._controller.userAccountHasLibraryAccess()
self.priority = 10
@ -53,7 +53,7 @@ class DigitalFactoryFileProvider(FileProvider):
if not self._dialog:
Logger.log("e", "Unable to create the Digital Library Open dialog.")
def _onLoginStateChanged(self, logged_in: bool) -> None:
def _onUserAccessStateChanged(self, logged_in: bool) -> None:
"""
Sets the enabled status of the DigitalFactoryFileProvider according to the account's login status
:param logged_in: The new login status

View file

@ -45,7 +45,7 @@ class DigitalFactoryOutputDevice(ProjectOutputDevice):
self._writing = False
self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account
self._account.loginStateChanged.connect(self._onLoginStateChanged)
self._controller.userAccessStateChanged.connect(self._onUserAccessStateChanged)
self.enabled = self._account.isLoggedIn and self._controller.userAccountHasLibraryAccess()
self._current_workspace_information = CuraApplication.getInstance().getCurrentWorkspaceInformation()
@ -97,7 +97,7 @@ class DigitalFactoryOutputDevice(ProjectOutputDevice):
if not self._dialog:
Logger.log("e", "Unable to create the Digital Library Save dialog.")
def _onLoginStateChanged(self, logged_in: bool) -> None:
def _onUserAccessStateChanged(self, logged_in: bool) -> None:
"""
Sets the enabled status of the DigitalFactoryOutputDevice according to the account's login status
:param logged_in: The new login status

View file

@ -1,3 +1,6 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from unittest.mock import MagicMock
import pytest
@ -37,7 +40,7 @@ def test_getProjectsFirstPage(api_client):
failed_callback = MagicMock()
# Call
api_client.getProjectsFirstPage(on_finished = finished_callback, failed = failed_callback)
api_client.getProjectsFirstPage(search_filter = "filter", on_finished = finished_callback, failed = failed_callback)
# Asserts
pagination_manager.reset.assert_called_once() # Should be called since we asked for new set of projects
@ -45,16 +48,16 @@ def test_getProjectsFirstPage(api_client):
args = http_manager.get.call_args_list[0]
# Ensure that it's called with the right limit
assert args[0][0] == "https://api.ultimaker.com/cura/v1/projects?limit=20"
assert args[0][0] == "https://api.ultimaker.com/cura/v1/projects?limit=20&search=filter"
# Change the limit & try again
http_manager.get.reset_mock()
pagination_manager.limit = 80
api_client.getProjectsFirstPage(on_finished = finished_callback, failed = failed_callback)
api_client.getProjectsFirstPage(search_filter = "filter", on_finished = finished_callback, failed = failed_callback)
args = http_manager.get.call_args_list[0]
# Ensure that it's called with the right limit
assert args[0][0] == "https://api.ultimaker.com/cura/v1/projects?limit=80"
assert args[0][0] == "https://api.ultimaker.com/cura/v1/projects?limit=80&search=filter"
def test_getMoreProjects_noNewProjects(api_client):

View file

@ -1,4 +1,4 @@
# Copyright (c) 2020 Ultimaker B.V.
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger
@ -103,20 +103,27 @@ class PerObjectSettingsTool(Tool):
new_instance.resetState() # Ensure that the state is not seen as a user state.
settings.addInstance(new_instance)
for property_key in ["top_bottom_thickness", "wall_thickness", "wall_line_count"]:
# Override some settings to ensure that the infill mesh by default adds no skin or walls. Or remove them if not an infill mesh.
specialized_settings = {
"top_bottom_thickness": 0,
"top_thickness": "=top_bottom_thickness",
"bottom_thickness": "=top_bottom_thickness",
"top_layers": "=0 if infill_sparse_density == 100 else math.ceil(round(top_thickness / resolveOrValue('layer_height'), 4))",
"bottom_layers": "=0 if infill_sparse_density == 100 else math.ceil(round(bottom_thickness / resolveOrValue('layer_height'), 4))",
"wall_thickness": 0,
"wall_line_count": "=max(1, round((wall_thickness - wall_line_width_0) / wall_line_width_x) + 1) if wall_thickness != 0 else 0"
}
for property_key in specialized_settings:
if mesh_type == "infill_mesh":
if settings.getInstance(property_key) is None:
definition = stack.getSettingDefinition(property_key)
new_instance = SettingInstance(definition, settings)
# We just want the wall_line count to be there in case it was overriden in the global stack.
# as such, we don't need to set a value.
if property_key != "wall_line_count":
new_instance.setProperty("value", 0)
new_instance.setProperty("value", specialized_settings[property_key])
new_instance.resetState() # Ensure that the state is not seen as a user state.
settings.addInstance(new_instance)
settings_visibility_changed = True
elif old_mesh_type == "infill_mesh" and settings.getInstance(property_key) and (settings.getProperty(property_key, "value") == 0 or property_key == "wall_line_count"):
elif old_mesh_type == "infill_mesh" and settings.getInstance(property_key) and property_key in specialized_settings:
settings.removeInstance(property_key)
settings_visibility_changed = True

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick 2.9
import QtQuick.Layouts 1.1
import QtQuick.Controls 2.3
@ -13,6 +13,8 @@ Item
{
id: prepareMenu
property var fileProviderModel: CuraApplication.getFileProviderModel()
UM.I18nCatalog
{
id: catalog
@ -23,24 +25,22 @@ Item
{
left: parent.left
right: parent.right
leftMargin: UM.Theme.getSize("wide_margin").width
rightMargin: UM.Theme.getSize("wide_margin").width
leftMargin: UM.Theme.getSize("wide_margin").width * 2
rightMargin: UM.Theme.getSize("wide_margin").width * 2
}
// Item to ensure that all of the buttons are nicely centered.
Item
{
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 2 * UM.Theme.getSize("wide_margin").width
height: parent.height
anchors.fill: parent
RowLayout
{
id: itemRow
anchors.left: openFileButton.right
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.leftMargin: UM.Theme.getSize("default_margin").width + openFileButton.width + openFileMenu.width
property int machineSelectorWidth: Math.round((width - printSetupSelectorItem.width) / 3)
height: parent.height
@ -52,9 +52,6 @@ Item
{
id: machineSelection
headerCornerSide: Cura.RoundedRectangle.Direction.Left
headerBackgroundBorder.width: UM.Theme.getSize("default_lining").width
headerBackgroundBorder.color: UM.Theme.getColor("lining")
enableHeaderShadow: false
Layout.preferredWidth: parent.machineSelectorWidth
Layout.fillWidth: true
Layout.fillHeight: true
@ -63,9 +60,6 @@ Item
Cura.ConfigurationMenu
{
id: printerSetup
enableHeaderShadow: false
headerBackgroundBorder.width: UM.Theme.getSize("default_lining").width
headerBackgroundBorder.color: UM.Theme.getColor("lining")
Layout.fillHeight: true
Layout.fillWidth: true
Layout.preferredWidth: parent.machineSelectorWidth * 2
@ -82,22 +76,129 @@ Item
}
}
//Pop-up shown when there are multiple items to select from.
Cura.ExpandablePopup
{
id: openFileMenu
visible: prepareMenu.fileProviderModel.count > 1
contentAlignment: Cura.ExpandablePopup.ContentAlignment.AlignLeft
headerCornerSide: Cura.RoundedRectangle.Direction.All
headerPadding: Math.round((parent.height - UM.Theme.getSize("button_icon").height) / 2)
contentPadding: UM.Theme.getSize("default_lining").width
enabled: visible
height: parent.height
width: visible ? (headerPadding * 3 + UM.Theme.getSize("button_icon").height + iconSize) : 0
headerItem: UM.RecolorImage
{
id: menuIcon
source: UM.Theme.getIcon("Folder", "medium")
color: UM.Theme.getColor("icon")
sourceSize.height: height
}
contentItem: Item
{
id: popup
Column
{
id: openProviderColumn
//The column doesn't automatically listen to its children rect if the children change internally, so we need to explicitly update the size.
onChildrenRectChanged:
{
popup.height = childrenRect.height
popup.width = childrenRect.width
}
onPositioningComplete:
{
popup.height = childrenRect.height
popup.width = childrenRect.width
}
Label
{
text: catalog.i18nc("@menu:header", "Open file")
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("medium")
renderType: Text.NativeRendering
verticalAlignment: Text.AlignVCenter
width: contentWidth
height: UM.Theme.getSize("action_button").height
leftPadding: UM.Theme.getSize("default_margin").width
}
Repeater
{
model: prepareMenu.fileProviderModel
delegate: Button
{
leftPadding: UM.Theme.getSize("default_margin").width
rightPadding: UM.Theme.getSize("default_margin").width
width: contentItem.width + leftPadding + rightPadding
height: UM.Theme.getSize("action_button").height
hoverEnabled: true
contentItem: Label
{
text: model.displayText
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("medium")
renderType: Text.NativeRendering
verticalAlignment: Text.AlignVCenter
width: contentWidth
height: parent.height
}
onClicked:
{
if(model.index == 0) //The 0th element is the "From Disk" option, which should activate the open local file dialog.
{
Cura.Actions.open.trigger();
}
else
{
prepareMenu.fileProviderModel.trigger(model.name);
}
}
background: Rectangle
{
color: parent.hovered ? UM.Theme.getColor("action_button_hovered") : "transparent"
radius: UM.Theme.getSize("action_button_radius").width
width: popup.width
}
}
}
}
}
}
//If there is just a single item, show a button instead that directly chooses the one option.
Button
{
id: openFileButton
height: UM.Theme.getSize("stage_menu").height
width: UM.Theme.getSize("stage_menu").height
visible: prepareMenu.fileProviderModel.count <= 1
height: parent.height
width: visible ? height : 0 //Square button (and don't take up space if invisible).
onClicked: Cura.Actions.open.trigger()
enabled: visible && prepareMenu.fileProviderModel.count > 0
hoverEnabled: true
contentItem: Item
{
anchors.fill: parent
UM.RecolorImage
{
id: buttonIcon
source: UM.Theme.getIcon("Folder", "medium")
anchors.centerIn: parent
source: UM.Theme.getIcon("Folder")
width: UM.Theme.getSize("button_icon").width
height: UM.Theme.getSize("button_icon").height
color: UM.Theme.getColor("icon")
@ -109,8 +210,8 @@ Item
background: Rectangle
{
id: background
height: UM.Theme.getSize("stage_menu").height
width: UM.Theme.getSize("stage_menu").height
height: parent.height
width: parent.width
border.color: UM.Theme.getColor("lining")
border.width: UM.Theme.getSize("default_lining").width

View file

@ -24,54 +24,36 @@ Item
{
left: parent.left
right: parent.right
leftMargin: UM.Theme.getSize("wide_margin").width
rightMargin: UM.Theme.getSize("wide_margin").width
leftMargin: UM.Theme.getSize("wide_margin").width * 2
rightMargin: UM.Theme.getSize("wide_margin").width * 2
}
Row
{
id: stageMenuRow
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 2 * UM.Theme.getSize("wide_margin").width
height: parent.height
anchors.fill: parent
// This is a trick to make sure that the borders of the two adjacent buttons' borders overlap. Otherwise
// there will be double border (one from each button)
spacing: -UM.Theme.getSize("default_lining").width
Cura.ViewsSelector
{
id: viewsSelector
height: parent.height
width: UM.Theme.getSize("views_selector").width
width: Math.max(Math.round((parent.width - printSetupSelectorItem.width) / 3), UM.Theme.getSize("views_selector").width)
headerCornerSide: Cura.RoundedRectangle.Direction.Left
}
// Separator line
Rectangle
{
height: parent.height
// If there is no viewPanel, we only need a single spacer, so hide this one.
visible: viewPanel.source != ""
width: visible ? UM.Theme.getSize("default_lining").width : 0
color: UM.Theme.getColor("lining")
}
// This component will grow freely up to complete the width of the row.
Loader
{
id: viewPanel
height: parent.height
width: source != "" ? (previewMenu.width - viewsSelector.width - printSetupSelectorItem.width - 2 * (UM.Theme.getSize("wide_margin").width + UM.Theme.getSize("default_lining").width)) : 0
width: source != "" ? (parent.width - viewsSelector.width - printSetupSelectorItem.width) : 0
source: UM.Controller.activeView != null && UM.Controller.activeView.stageMenuComponent != null ? UM.Controller.activeView.stageMenuComponent : ""
}
// Separator line
Rectangle
{
height: parent.height
width: UM.Theme.getSize("default_lining").width
color: UM.Theme.getColor("lining")
}
Item
{
id: printSetupSelectorItem

View file

@ -203,16 +203,16 @@ Cura.ExpandableComponent
style: UM.Theme.styles.checkbox
UM.RecolorImage
Rectangle
{
id: swatch
anchors.verticalCenter: parent.verticalCenter
anchors.right: extrudersModelCheckBox.right
width: UM.Theme.getSize("layerview_legend_size").width
height: UM.Theme.getSize("layerview_legend_size").height
source: UM.Theme.getIcon("Extruder", "medium")
color: model.color
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
}
Label

View file

@ -1,23 +1,26 @@
// Copyright (c) 2019 Ultimaker B.V.
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.3
import QtQuick.Controls 1.4
import QtQuick.Controls 2.4
import QtQuick.Controls.Styles 1.3
import UM 1.3 as UM
import Cura 1.0 as Cura
Rectangle
Button
{
id: base
property var enabled: true
property var iconSource: null
color: enabled ? UM.Theme.getColor("monitor_icon_primary") : UM.Theme.getColor("monitor_icon_disabled")
height: width
radius: Math.round(0.5 * width)
width: 24 * screenScaleFactor
width: UM.Theme.getSize("button").width * 0.75 //Matching the size of the content of tool buttons.
height: UM.Theme.getSize("button").height * 0.75
hoverEnabled: true
background: Rectangle
{
anchors.fill: parent
radius: 0.5 * width
color: parent.enabled ? (parent.hovered ? UM.Theme.getColor("monitor_secondary_button_hover") : "transparent") : UM.Theme.getColor("monitor_icon_disabled")
}
UM.RecolorImage
{
@ -27,30 +30,21 @@ Rectangle
horizontalCenter: parent.horizontalCenter
verticalCenter: parent.verticalCenter
}
color: UM.Theme.getColor("monitor_icon_accent")
color: UM.Theme.getColor("primary")
height: width
source: iconSource
width: Math.round(parent.width / 2)
}
MouseArea
onClicked:
{
id: clickArea
anchors.fill: parent
hoverEnabled: base.enabled
onClicked:
if (OutputDevice.activeCameraUrl != "")
{
if (base.enabled)
{
if (OutputDevice.activeCameraUrl != "")
{
OutputDevice.setActiveCameraUrl("")
}
else
{
OutputDevice.setActiveCameraUrl(modelData.cameraUrl)
}
}
OutputDevice.setActiveCameraUrl("")
}
else
{
OutputDevice.setActiveCameraUrl(modelData.cameraUrl)
}
}
}

View file

@ -50,7 +50,7 @@ Item
id: buildplateIcon
anchors.centerIn: parent
color: UM.Theme.getColor("monitor_icon_primary")
height: parent.height
height: UM.Theme.getSize("medium_button_icon").width
source: "../svg/icons/Buildplate.svg"
width: height
visible: buildplate

View file

@ -5,6 +5,8 @@ import QtQuick 2.2
import QtQuick.Controls 2.0
import UM 1.3 as UM
import Cura 1.6 as Cura
/**
* This component comprises a colored extruder icon, the material name, and the
* print core name. It is used by the MonitorPrinterConfiguration component with
@ -18,10 +20,10 @@ import UM 1.3 as UM
Item
{
// The material color
property alias color: extruderIcon.color
property alias color: extruderIcon.materialColor
// The extruder position; NOTE: Decent human beings count from 0
property alias position: extruderIcon.position
// The extruder position
property int position
// The material name
property alias material: materialLabel.text
@ -32,12 +34,13 @@ Item
// Height is 2 x 18px labels, plus 4px spacing between them
height: 40 * screenScaleFactor // TODO: Theme!
width: childrenRect.width
opacity: material != "" && material != "Empty" && position >= 0 ? 1 : 0.4
MonitorIconExtruder
Cura.ExtruderIcon
{
id: extruderIcon
color: UM.Theme.getColor("monitor_skeleton_loading")
position: 0
materialColor: UM.Theme.getColor("monitor_skeleton_loading")
anchors.verticalCenter: parent.verticalCenter
}
Rectangle
@ -46,16 +49,18 @@ Item
anchors
{
left: extruderIcon.right
leftMargin: 12 * screenScaleFactor // TODO: Theme!
leftMargin: UM.Theme.getSize("default_margin").width
verticalCenter: extruderIcon.verticalCenter
}
color: materialLabel.visible > 0 ? "transparent" : UM.Theme.getColor("monitor_skeleton_loading")
height: 18 * screenScaleFactor // TODO: Theme!
height: childrenRect.height
width: Math.max(materialLabel.contentWidth, 60 * screenScaleFactor) // TODO: Theme!
radius: 2 * screenScaleFactor // TODO: Theme!
Label
{
id: materialLabel
anchors.top: parent.top
color: UM.Theme.getColor("text")
elide: Text.ElideRight
@ -63,29 +68,13 @@ Item
text: ""
visible: text !== ""
// FIXED-LINE-HEIGHT:
height: parent.height
verticalAlignment: Text.AlignVCenter
renderType: Text.NativeRendering
}
}
Rectangle
{
id: printCoreLabelWrapper
anchors
{
left: materialLabelWrapper.left
bottom: parent.bottom
}
color: printCoreLabel.visible > 0 ? "transparent" : UM.Theme.getColor("monitor_skeleton_loading")
height: 18 * screenScaleFactor // TODO: Theme!
width: Math.max(printCoreLabel.contentWidth, 36 * screenScaleFactor) // TODO: Theme!
radius: 2 * screenScaleFactor // TODO: Theme!
Label
{
id: printCoreLabel
anchors.top: materialLabel.bottom
color: UM.Theme.getColor("text")
elide: Text.ElideRight
@ -93,9 +82,6 @@ Item
text: ""
visible: text !== ""
// FIXED-LINE-HEIGHT:
height: parent.height
verticalAlignment: Text.AlignVCenter
renderType: Text.NativeRendering
}
}

View file

@ -38,6 +38,7 @@ Item
Label
{
id: positionLabel
anchors.centerIn: icon
font: UM.Theme.getFont("small")
color: UM.Theme.getColor("text")
height: Math.round(size / 2)
@ -45,8 +46,6 @@ Item
text: position + 1
verticalAlignment: Text.AlignVCenter
width: Math.round(size / 2)
x: Math.round(size * 0.25)
y: Math.round(size * 0.15625)
visible: position >= 0
renderType: Text.NativeRendering
}

View file

@ -256,7 +256,16 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
"""
self._uploaded_print_job = self._pre_upload_print_job
self._progress.hide()
PrintJobUploadSuccessMessage().show()
message = PrintJobUploadSuccessMessage()
message.addAction("monitor print",
name=I18N_CATALOG.i18nc("@action:button", "Monitor print"),
icon="",
description=I18N_CATALOG.i18nc("@action:tooltip", "Track the print in Ultimaker Digital Factory"),
button_align=message.ActionButtonAlignment.ALIGN_RIGHT)
df_url = f"https://digitalfactory.ultimaker.com/app/jobs/{self._cluster.cluster_id}?utm_source=cura&utm_medium=software&utm_campaign=monitor-button"
message.pyQtActionTriggered.connect(lambda message, action: (QDesktopServices.openUrl(QUrl(df_url)), message.hide()))
message.show()
self.writeFinished.emit()
def _onPrintUploadSpecificError(self, reply: "QNetworkReply", _: "QNetworkReply.NetworkError"):

View file

@ -13,6 +13,5 @@ class PrintJobUploadSuccessMessage(Message):
def __init__(self) -> None:
super().__init__(
text = I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer."),
title = I18N_CATALOG.i18nc("@info:title", "Data Sent"),
lifetime = 5
title = I18N_CATALOG.i18nc("@info:title", "Data Sent")
)