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;}
-
+