diff --git a/plugins/DigitalLibrary/resources/images/projects_not_found.svg b/plugins/DigitalLibrary/resources/images/projects_not_found.svg new file mode 100644 index 0000000000..8aee7b797c --- /dev/null +++ b/plugins/DigitalLibrary/resources/images/projects_not_found.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml b/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml index a4eefd13f6..98b0c5ca02 100644 --- a/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml +++ b/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml @@ -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,48 +31,58 @@ 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" - visible: createNewProjectButtonVisible && manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed) - - 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" + visible: createNewProjectButtonVisible && manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed) + + onClicked: + { + createNewProjectPopup.open() + } + busy: manager.creatingNewProjectStatus == DF.RetrievalStatus.InProgress } - busy: manager.creatingNewProjectStatus == DF.RetrievalStatus.InProgress - } - Cura.SecondaryButton - { - id: upgradePlanButton + Cura.SecondaryButton + { + id: upgradePlanButton - anchors.verticalCenter: selectProjectLabel.verticalCenter - anchors.right: parent.right - text: "Upgrade plan" - iconSource: UM.Theme.getIcon("LinkExternal") - visible: createNewProjectButtonVisible && !manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed) - tooltip: "You have reached the maximum number of projects allowed by your subscription. Please upgrade to the Professional subscription to create more projects." - tooltipWidth: parent.width * 0.5 + text: "Upgrade plan" + iconSource: UM.Theme.getIcon("LinkExternal") + visible: createNewProjectButtonVisible && !manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed) + tooltip: "You have reached the maximum number of projects allowed by your subscription. Please upgrade to the Professional subscription to create more projects." + tooltipWidth: parent.width * 0.5 - onClicked: Qt.openUrlExternally("https://ultimaker.com/software/enterprise-software") + onClicked: Qt.openUrlExternally("https://ultimaker.com/software/enterprise-software") + } } Item @@ -93,7 +105,7 @@ 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 @@ -104,8 +116,9 @@ Item { 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 @@ -114,6 +127,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. } } } @@ -123,7 +137,7 @@ Item id: projectListContainer anchors { - top: selectProjectLabel.bottom + top: headerRow.bottom topMargin: UM.Theme.getSize("default_margin").height bottom: parent.bottom left: parent.left diff --git a/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py b/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py index 460438e365..9a3157ccd6 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py @@ -132,7 +132,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. @@ -140,13 +140,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, diff --git a/plugins/DigitalLibrary/src/DigitalFactoryController.py b/plugins/DigitalLibrary/src/DigitalFactoryController.py index 5caaca2e25..cd0f0be638 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryController.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryController.py @@ -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 @@ -119,6 +121,11 @@ class DigitalFactoryController(QObject): 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() @@ -178,7 +185,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: """ @@ -304,6 +311,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 @@ -518,7 +557,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._api.checkUserCanCreateNewLibraryProject(callback = self.setCanCreateNewLibraryProject) self.setRetrievingProjectsStatus(RetrievalStatus.InProgress) self._has_preselected_project = new_has_preselected_project diff --git a/plugins/DigitalLibrary/tests/TestDigitalLibraryApiClient.py b/plugins/DigitalLibrary/tests/TestDigitalLibraryApiClient.py index ba0a0b15b4..9751838ddf 100644 --- a/plugins/DigitalLibrary/tests/TestDigitalLibraryApiClient.py +++ b/plugins/DigitalLibrary/tests/TestDigitalLibraryApiClient.py @@ -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): diff --git a/resources/definitions/atom2.def.json b/resources/definitions/atom2.def.json new file mode 100644 index 0000000000..d7a26546d8 --- /dev/null +++ b/resources/definitions/atom2.def.json @@ -0,0 +1,35 @@ +{ + "name": "Atom 2", + "version": 2, + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Victor (Yu Chieh) Lin", + "manufacturer": "Layer One", + "file_formats": "text/x-gcode", + "platform_offset": [0,0,0], + "machine_extruder_trains": { "0": "atom2_extruder_0" + } + }, + + "overrides": { + "machine_name": { "default_value": "Atom 2" }, + "machine_shape": { "default_value": "elliptic" }, + "machine_width": { "default_value": 210 }, + "machine_depth": { "default_value": 210 }, + "machine_height": { "default_value": 320 }, + "machine_extruder_count": { "default_value": 1 }, + "machine_heated_bed": { "default_value": false }, + "machine_center_is_zero": { "default_value": true }, + + "machine_start_gcode": { "default_value": "G21\nG90 \nM107\nG28\nG92 E0\nG1 F200 E3\nG92 E0" }, + "machine_end_gcode": { "default_value": "M104 S0\nG28\nG91\nG1 E-6 F300\nM84\nG90" }, + + "layer_height": { "default_value": 0.2 }, + "default_material_print_temperature": { "default_value": 210 }, + "speed_print": { "default_value": 32 }, + "optimize_wall_printing_order": { "value": "True" }, + "infill_sparse_density": { "default_value": 10 }, + "brim_width": { "default_value": 4 } + } +} diff --git a/resources/extruders/atom2_extruder_0.def.json b/resources/extruders/atom2_extruder_0.def.json new file mode 100644 index 0000000000..be9d5782ff --- /dev/null +++ b/resources/extruders/atom2_extruder_0.def.json @@ -0,0 +1,15 @@ +{ + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "atom2", + "position": "0" + }, + + "overrides": { + "extruder_nr": { "default_value": 0 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} diff --git a/resources/qml/Widgets/TextField.qml b/resources/qml/Widgets/TextField.qml index 28074d4415..c126c8a6e0 100644 --- a/resources/qml/Widgets/TextField.qml +++ b/resources/qml/Widgets/TextField.qml @@ -1,4 +1,4 @@ -// 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.10 @@ -15,6 +15,8 @@ TextField { id: textField + property alias leftIcon: iconLeft.source + UM.I18nCatalog { id: catalog; name: "cura" } hoverEnabled: true @@ -22,6 +24,7 @@ TextField font: UM.Theme.getFont("default") color: UM.Theme.getColor("text") renderType: Text.NativeRendering + leftPadding: iconLeft.visible ? iconLeft.width + UM.Theme.getSize("default_margin").width * 2 : UM.Theme.getSize("thin_margin").width states: [ State @@ -52,7 +55,6 @@ TextField color: UM.Theme.getColor("main_background") - anchors.margins: Math.round(UM.Theme.getSize("default_lining").width) radius: UM.Theme.getSize("setting_control_radius").width border.color: @@ -67,5 +69,23 @@ TextField } return UM.Theme.getColor("setting_control_border") } + + //Optional icon added on the left hand side. + UM.RecolorImage + { + id: iconLeft + + anchors + { + verticalCenter: parent.verticalCenter + left: parent.left + leftMargin: UM.Theme.getSize("default_margin").width + } + + visible: source != "" + height: UM.Theme.getSize("small_button_icon").height + width: visible ? height : 0 + color: textField.color + } } } diff --git a/resources/themes/cura-light/icons/medium/ExtruderColor.svg b/resources/themes/cura-light/icons/medium/ExtruderColor.svg index 85360a9622..cd4452b246 100644 --- a/resources/themes/cura-light/icons/medium/ExtruderColor.svg +++ b/resources/themes/cura-light/icons/medium/ExtruderColor.svg @@ -6,8 +6,8 @@ .st0{fill:#231F20;} - +