1// Copyright (c) 2017 Ultimaker B.V.
2// Cura is released under the terms of the LGPLv3 or higher.
3
4import QtQuick 2.7
5import QtQuick.Controls 1.4
6import QtQuick.Dialogs 1.2
7
8import UM 1.2 as UM
9import Cura 1.0 as Cura
10
11import ".." // Access to ReadOnlyTextArea.qml
12
13TabView
14{
15    id: base
16
17    property QtObject properties
18    property var currentMaterialNode: null
19
20    property bool editingEnabled: false
21    property string currency: UM.Preferences.getValue("cura/currency") ? UM.Preferences.getValue("cura/currency") : "€"
22    property real firstColumnWidth: (width * 0.50) | 0
23    property real secondColumnWidth: (width * 0.40) | 0
24    property string containerId: ""
25    property var materialPreferenceValues: UM.Preferences.getValue("cura/material_settings") ? JSON.parse(UM.Preferences.getValue("cura/material_settings")) : {}
26    property var materialManagementModel: CuraApplication.getMaterialManagementModel()
27
28    property double spoolLength: calculateSpoolLength()
29    property real costPerMeter: calculateCostPerMeter()
30
31    signal resetSelectedMaterial()
32
33    property bool reevaluateLinkedMaterials: false
34    property string linkedMaterialNames:
35    {
36        if (reevaluateLinkedMaterials)
37        {
38            reevaluateLinkedMaterials = false;
39        }
40        if (!base.containerId || !base.editingEnabled || !base.currentMaterialNode)
41        {
42            return ""
43        }
44        var linkedMaterials = Cura.ContainerManager.getLinkedMaterials(base.currentMaterialNode, true);
45        if (linkedMaterials.length == 0)
46        {
47            return ""
48        }
49        return linkedMaterials;
50    }
51
52    function getApproximateDiameter(diameter)
53    {
54        return Math.round(diameter);
55    }
56
57    // This trick makes sure to make all fields lose focus so their onEditingFinished will be triggered
58    // and modified values will be saved. This can happen when a user changes a value and then closes the
59    // dialog directly.
60    //
61    // Please note that somehow this callback is ONLY triggered when visible is false.
62    onVisibleChanged:
63    {
64        if (!visible)
65        {
66            base.focus = false;
67        }
68    }
69
70    Tab
71    {
72        title: catalog.i18nc("@title", "Information")
73
74        anchors.margins: UM.Theme.getSize("default_margin").width
75
76        ScrollView
77        {
78            id: scrollView
79            anchors.fill: parent
80            horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff
81            flickableItem.flickableDirection: Flickable.VerticalFlick
82            frameVisible: true
83
84            property real columnWidth: (viewport.width * 0.5 - UM.Theme.getSize("default_margin").width) | 0
85
86            Flow
87            {
88                id: containerGrid
89
90                x: UM.Theme.getSize("default_margin").width
91                y: UM.Theme.getSize("default_lining").height
92
93                width: base.width
94                property real rowHeight: brandTextField.height + UM.Theme.getSize("default_lining").height
95
96                MessageDialog
97                {
98                    id: confirmDiameterChangeDialog
99
100                    icon: StandardIcon.Question;
101                    title: catalog.i18nc("@title:window", "Confirm Diameter Change")
102                    text: catalog.i18nc("@label (%1 is a number)", "The new filament diameter is set to %1 mm, which is not compatible with the current extruder. Do you wish to continue?".arg(new_diameter_value))
103                    standardButtons: StandardButton.Yes | StandardButton.No
104                    modality: Qt.ApplicationModal
105
106                    property var new_diameter_value: null;
107                    property var old_diameter_value: null;
108                    property var old_approximate_diameter_value: null;
109
110                    onYes:
111                    {
112                        base.setMetaDataEntry("approximate_diameter", old_approximate_diameter_value, getApproximateDiameter(new_diameter_value).toString());
113                        base.setMetaDataEntry("properties/diameter", properties.diameter, new_diameter_value);
114                        // CURA-6868 Make sure to update the extruder to user a diameter-compatible material.
115                        Cura.MachineManager.updateMaterialWithVariant()
116                        base.resetSelectedMaterial()
117                    }
118
119                    onNo:
120                    {
121                        base.properties.diameter = old_diameter_value;
122                        diameterSpinBox.value = Qt.binding(function() { return base.properties.diameter })
123                    }
124
125                    onRejected: no()
126                }
127
128                Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Display Name") }
129                ReadOnlyTextField
130                {
131                    id: displayNameTextField;
132                    width: scrollView.columnWidth;
133                    text: properties.name;
134                    readOnly: !base.editingEnabled;
135                    onEditingFinished: base.updateMaterialDisplayName(properties.name, text)
136                }
137
138                Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Brand") }
139                ReadOnlyTextField
140                {
141                    id: brandTextField;
142                    width: scrollView.columnWidth;
143                    text: properties.brand;
144                    readOnly: !base.editingEnabled;
145                    onEditingFinished: base.updateMaterialBrand(properties.brand, text)
146                }
147
148                Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Material Type") }
149                ReadOnlyTextField
150                {
151                    id: materialTypeField;
152                    width: scrollView.columnWidth;
153                    text: properties.material;
154                    readOnly: !base.editingEnabled;
155                    onEditingFinished: base.updateMaterialType(properties.material, text)
156                }
157
158                Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Color") }
159                Row
160                {
161                    width: scrollView.columnWidth
162                    height:  parent.rowHeight
163                    spacing: Math.round(UM.Theme.getSize("default_margin").width / 2)
164
165                    // color indicator square
166                    Rectangle
167                    {
168                        id: colorSelector
169                        color: properties.color_code
170
171                        width: Math.round(colorLabel.height * 0.75)
172                        height: Math.round(colorLabel.height * 0.75)
173                        border.width: UM.Theme.getSize("default_lining").height
174
175                        anchors.verticalCenter: parent.verticalCenter
176
177                        // open the color selection dialog on click
178                        MouseArea
179                        {
180                            anchors.fill: parent
181                            onClicked: colorDialog.open()
182                            enabled: base.editingEnabled
183                        }
184                    }
185
186                    // pretty color name text field
187                    ReadOnlyTextField
188                    {
189                        id: colorLabel;
190                        width: parent.width - colorSelector.width - parent.spacing
191                        text: properties.color_name;
192                        readOnly: !base.editingEnabled
193                        onEditingFinished: base.setMetaDataEntry("color_name", properties.color_name, text)
194                    }
195
196                    // popup dialog to select a new color
197                    // if successful it sets the properties.color_code value to the new color
198                    ColorDialog
199                    {
200                        id: colorDialog
201                        color: properties.color_code
202                        onAccepted: base.setMetaDataEntry("color_code", properties.color_code, color)
203                    }
204                }
205
206                Item { width: parent.width; height: UM.Theme.getSize("default_margin").height }
207
208                Label { width: parent.width; height: parent.rowHeight; font.bold: true; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Properties") }
209
210                Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Density") }
211                ReadOnlySpinBox
212                {
213                    id: densitySpinBox
214                    width: scrollView.columnWidth
215                    value: properties.density
216                    decimals: 2
217                    suffix: " g/cm³"
218                    stepSize: 0.01
219                    readOnly: !base.editingEnabled
220
221                    onEditingFinished: base.setMetaDataEntry("properties/density", properties.density, value)
222                    onValueChanged: updateCostPerMeter()
223                }
224
225                Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Diameter") }
226                ReadOnlySpinBox
227                {
228                    id: diameterSpinBox
229                    width: scrollView.columnWidth
230                    value: properties.diameter
231                    decimals: 2
232                    suffix: " mm"
233                    stepSize: 0.01
234                    readOnly: !base.editingEnabled
235
236                    onEditingFinished:
237                    {
238                        // This does not use a SettingPropertyProvider, because we need to make the change to all containers
239                        // which derive from the same base_file
240                        var old_diameter = Cura.ContainerManager.getContainerMetaDataEntry(base.containerId, "properties/diameter");
241                        var old_approximate_diameter = Cura.ContainerManager.getContainerMetaDataEntry(base.containerId, "approximate_diameter");
242                        var new_approximate_diameter = getApproximateDiameter(value);
243                        if (new_approximate_diameter != Cura.ExtruderManager.getActiveExtruderStack().approximateMaterialDiameter)
244                        {
245                            confirmDiameterChangeDialog.old_diameter_value = old_diameter;
246                            confirmDiameterChangeDialog.new_diameter_value = value;
247                            confirmDiameterChangeDialog.old_approximate_diameter_value = old_approximate_diameter;
248
249                            confirmDiameterChangeDialog.open()
250                        }
251                        else {
252                            base.setMetaDataEntry("approximate_diameter", old_approximate_diameter, getApproximateDiameter(value).toString());
253                            base.setMetaDataEntry("properties/diameter", properties.diameter, value);
254                        }
255                    }
256                    onValueChanged: updateCostPerMeter()
257                }
258
259                Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Filament Cost") }
260                SpinBox
261                {
262                    id: spoolCostSpinBox
263                    width: scrollView.columnWidth
264                    value: base.getMaterialPreferenceValue(properties.guid, "spool_cost")
265                    prefix: base.currency + " "
266                    decimals: 2
267                    maximumValue: 100000000
268
269                    onValueChanged:
270                    {
271                        base.setMaterialPreferenceValue(properties.guid, "spool_cost", parseFloat(value))
272                        updateCostPerMeter()
273                    }
274                }
275
276                Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Filament weight") }
277                SpinBox
278                {
279                    id: spoolWeightSpinBox
280                    width: scrollView.columnWidth
281                    value: base.getMaterialPreferenceValue(properties.guid, "spool_weight", Cura.ContainerManager.getContainerMetaDataEntry(properties.container_id, "properties/weight"))
282                    suffix: " g"
283                    stepSize: 100
284                    decimals: 0
285                    maximumValue: 10000
286
287                    onValueChanged:
288                    {
289                        base.setMaterialPreferenceValue(properties.guid, "spool_weight", parseFloat(value))
290                        updateCostPerMeter()
291                    }
292                }
293
294                Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Filament length") }
295                Label
296                {
297                    width: scrollView.columnWidth
298                    text: "~ %1 m".arg(Math.round(base.spoolLength))
299                    verticalAlignment: Qt.AlignVCenter
300                    height: parent.rowHeight
301                }
302
303                Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Cost per Meter") }
304                Label
305                {
306                    width: scrollView.columnWidth
307                    text: "~ %1 %2/m".arg(base.costPerMeter.toFixed(2)).arg(base.currency)
308                    verticalAlignment: Qt.AlignVCenter
309                    height: parent.rowHeight
310                }
311
312                Item { width: parent.width; height: UM.Theme.getSize("default_margin").height; visible: unlinkMaterialButton.visible }
313                Label
314                {
315                    width: 2 * scrollView.columnWidth
316                    verticalAlignment: Qt.AlignVCenter
317                    text: catalog.i18nc("@label", "This material is linked to %1 and shares some of its properties.").arg(base.linkedMaterialNames)
318                    wrapMode: Text.WordWrap
319                    visible: unlinkMaterialButton.visible
320                }
321                Button
322                {
323                    id: unlinkMaterialButton
324                    text: catalog.i18nc("@label", "Unlink Material")
325                    visible: base.linkedMaterialNames != ""
326                    onClicked:
327                    {
328                        Cura.ContainerManager.unlinkMaterial(base.currentMaterialNode)
329                        base.reevaluateLinkedMaterials = true
330                    }
331                }
332
333                Item { width: parent.width; height: UM.Theme.getSize("default_margin").height }
334
335                Label { width: parent.width; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Description") }
336
337                ReadOnlyTextArea
338                {
339                    text: properties.description;
340                    width: 2 * scrollView.columnWidth
341                    wrapMode: Text.WordWrap
342
343                    readOnly: !base.editingEnabled;
344
345                    onEditingFinished: base.setMetaDataEntry("description", properties.description, text)
346                }
347
348                Label { width: parent.width; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Adhesion Information") }
349
350                ReadOnlyTextArea
351                {
352                    text: properties.adhesion_info;
353                    width: 2 * scrollView.columnWidth
354                    wrapMode: Text.WordWrap
355
356                    readOnly: !base.editingEnabled;
357
358                    onEditingFinished: base.setMetaDataEntry("adhesion_info", properties.adhesion_info, text)
359                }
360
361                Item { width: parent.width; height: UM.Theme.getSize("default_margin").height }
362            }
363
364            function updateCostPerMeter()
365            {
366                base.spoolLength = calculateSpoolLength(diameterSpinBox.value, densitySpinBox.value, spoolWeightSpinBox.value);
367                base.costPerMeter = calculateCostPerMeter(spoolCostSpinBox.value);
368            }
369        }
370    }
371
372    Tab
373    {
374        title: catalog.i18nc("@label", "Print settings")
375        anchors
376        {
377            leftMargin: UM.Theme.getSize("default_margin").width
378            topMargin: UM.Theme.getSize("default_margin").height
379            bottomMargin: UM.Theme.getSize("default_margin").height
380            rightMargin: 0
381        }
382
383        ScrollView
384        {
385            anchors.fill: parent;
386
387            ListView
388            {
389                model: UM.SettingDefinitionsModel
390                {
391                    containerId: Cura.MachineManager.activeMachine != null ? Cura.MachineManager.activeMachine.definition.id: ""
392                    visibilityHandler: Cura.MaterialSettingsVisibilityHandler { }
393                    expanded: ["*"]
394                }
395
396                delegate: UM.TooltipArea
397                {
398                    width: childrenRect.width
399                    height: childrenRect.height
400                    text: model.description
401                    Label
402                    {
403                        id: label
404                        width: base.firstColumnWidth;
405                        height: spinBox.height + UM.Theme.getSize("default_lining").height
406                        text: model.label
407                        elide: Text.ElideRight
408                        verticalAlignment: Qt.AlignVCenter
409                    }
410                    ReadOnlySpinBox
411                    {
412                        id: spinBox
413                        anchors.left: label.right
414                        value:
415                        {
416                            // In case the setting is not in the material...
417                            if (!isNaN(parseFloat(materialPropertyProvider.properties.value)))
418                            {
419                                return parseFloat(materialPropertyProvider.properties.value);
420                            }
421                            // ... we search in the variant, and if it is not there...
422                            if (!isNaN(parseFloat(variantPropertyProvider.properties.value)))
423                            {
424                                return parseFloat(variantPropertyProvider.properties.value);
425                            }
426                            // ... then look in the definition container.
427                            if (!isNaN(parseFloat(machinePropertyProvider.properties.value)))
428                            {
429                                return parseFloat(machinePropertyProvider.properties.value);
430                            }
431                            return 0;
432                        }
433                        width: base.secondColumnWidth
434                        readOnly: !base.editingEnabled
435                        suffix: " " + model.unit
436                        maximumValue: 99999
437                        decimals: model.unit == "mm" ? 2 : 0
438
439                        onEditingFinished: materialPropertyProvider.setPropertyValue("value", value)
440                    }
441
442                    UM.ContainerPropertyProvider
443                    {
444                        id: materialPropertyProvider
445                        containerId: base.containerId
446                        watchedProperties: [ "value" ]
447                        key: model.key
448                    }
449                    UM.ContainerPropertyProvider
450                    {
451                        id: variantPropertyProvider
452                        containerId: Cura.MachineManager.activeStack.variant.id
453                        watchedProperties: [ "value" ]
454                        key: model.key
455                    }
456                    UM.ContainerPropertyProvider
457                    {
458                        id: machinePropertyProvider
459                        containerId: Cura.MachineManager.activeMachine != null ? Cura.MachineManager.activeMachine.definition.id: ""
460                        watchedProperties: [ "value" ]
461                        key: model.key
462                    }
463                }
464            }
465        }
466    }
467
468    function calculateSpoolLength(diameter, density, spoolWeight)
469    {
470        if(!diameter)
471        {
472            diameter = properties.diameter;
473        }
474        if(!density)
475        {
476            density = properties.density;
477        }
478        if(!spoolWeight)
479        {
480            spoolWeight = base.getMaterialPreferenceValue(properties.guid, "spool_weight", Cura.ContainerManager.getContainerMetaDataEntry(properties.container_id, "properties/weight"));
481        }
482
483        if (diameter == 0 || density == 0 || spoolWeight == 0)
484        {
485            return 0;
486        }
487        var area = Math.PI * Math.pow(diameter / 2, 2); // in mm2
488        var volume = (spoolWeight / density); // in cm3
489        return volume / area; // in m
490    }
491
492    function calculateCostPerMeter(spoolCost)
493    {
494        if(!spoolCost)
495        {
496            spoolCost = base.getMaterialPreferenceValue(properties.guid, "spool_cost");
497        }
498
499        if (spoolLength == 0)
500        {
501            return 0;
502        }
503        return spoolCost / spoolLength;
504    }
505
506    // Tiny convenience function to check if a value really changed before trying to set it.
507    function setMetaDataEntry(entry_name, old_value, new_value)
508    {
509        if (old_value != new_value)
510        {
511            Cura.ContainerManager.setContainerMetaDataEntry(base.currentMaterialNode, entry_name, new_value)
512            // make sure the UI properties are updated as well since we don't re-fetch the entire model here
513            // When the entry_name is something like properties/diameter, we take the last part of the entry_name
514            var list = entry_name.split("/")
515            var key = list[list.length - 1]
516            properties[key] = new_value
517        }
518    }
519
520    function setMaterialPreferenceValue(material_guid, entry_name, new_value)
521    {
522        if(!(material_guid in materialPreferenceValues))
523        {
524            materialPreferenceValues[material_guid] = {};
525        }
526        if(entry_name in materialPreferenceValues[material_guid] && materialPreferenceValues[material_guid][entry_name] == new_value)
527        {
528            // value has not changed
529            return;
530        }
531        if (entry_name in materialPreferenceValues[material_guid] && new_value.toString() == 0)
532        {
533            // no need to store a 0, that's the default, so remove it
534            materialPreferenceValues[material_guid].delete(entry_name);
535            if (!(materialPreferenceValues[material_guid]))
536            {
537                // remove empty map
538                materialPreferenceValues.delete(material_guid);
539            }
540        }
541        if (new_value.toString() != 0)
542        {
543            // store new value
544            materialPreferenceValues[material_guid][entry_name] = new_value;
545        }
546
547        // store preference
548        UM.Preferences.setValue("cura/material_settings", JSON.stringify(materialPreferenceValues));
549    }
550
551    function getMaterialPreferenceValue(material_guid, entry_name, default_value)
552    {
553        if(material_guid in materialPreferenceValues && entry_name in materialPreferenceValues[material_guid])
554        {
555            return materialPreferenceValues[material_guid][entry_name];
556        }
557        default_value = default_value | 0;
558        return default_value;
559    }
560
561    // update the display name of the material
562    function updateMaterialDisplayName(old_name, new_name)
563    {
564        // don't change when new name is the same
565        if (old_name == new_name)
566        {
567            return
568        }
569
570        // update the values
571        base.materialManagementModel.setMaterialName(base.currentMaterialNode, new_name)
572        properties.name = new_name
573    }
574
575    // update the type of the material
576    function updateMaterialType(old_type, new_type)
577    {
578        base.setMetaDataEntry("material", old_type, new_type)
579        properties.material = new_type
580    }
581
582    // update the brand of the material
583    function updateMaterialBrand(old_brand, new_brand)
584    {
585        base.setMetaDataEntry("brand", old_brand, new_brand)
586        properties.brand = new_brand
587    }
588}
589