From 2cbaece3e1472639c29a5b2d7f193b62eabcadf9 Mon Sep 17 00:00:00 2001 From: pinchies <> Date: Fri, 26 Oct 2018 22:21:01 +1100 Subject: [PATCH 001/146] Add JGAurora Z-603S Add a preset for the JGAurora Z-603S to model list --- .../definitions/jgaurora_z_603s.def.json | 96 +++++++++++++++++++ .../jgaurora_z_603s_extruder_0.def.json | 16 ++++ 2 files changed, 112 insertions(+) create mode 100644 resources/definitions/jgaurora_z_603s.def.json create mode 100644 resources/extruders/jgaurora_z_603s_extruder_0.def.json diff --git a/resources/definitions/jgaurora_z_603s.def.json b/resources/definitions/jgaurora_z_603s.def.json new file mode 100644 index 0000000000..af7fa823db --- /dev/null +++ b/resources/definitions/jgaurora_z_603s.def.json @@ -0,0 +1,96 @@ +{ + "name": "JGAurora Z-603S", + "version": 2, + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Samuel Pinches", + "manufacturer": "JGAurora", + "file_formats": "text/x-gcode", + "preferred_quality_type": "fine", + "machine_extruder_trains": + { + "0": "jgaurora_z_603s_extruder_0" + } + }, + "overrides": { + "machine_name": { + "default_value": "JGAurora Z-603S" + }, + "machine_start_gcode": { + "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 X0 Y0 ;move to the X/Y origin (Home)\nG28 Z0 ;move to the Z origin (Home)\nG1 Z15.0 F6000 ;move Z to position 15.0 mm as fast as possible\nG92 E0 ;zero the extruded length\nG1 X0.0 Y0.0 F1000.0 ;go to edge of print area\nG1 X60.0 E9.0 F1000.0 ;intro line\nG1 X100.0 E21.5 F1000.0 ;intro line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" + }, + "machine_end_gcode": { + "default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nM140 S0 ;turn off bed heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 ;move to the X-axis origin (Home)\nG0 Y280 F600 ;bring the bed to the front for easy print removal\nM84 ;turn off stepper motors\n; -- end of END GCODE --" + }, + "machine_width": { + "default_value": 280 + }, + "machine_height": { + "default_value": 175 + }, + "machine_depth": { + "default_value": 180 + }, + "machine_heated_bed": { + "default_value": true + }, + "machine_center_is_zero": { + "default_value": false + }, + "gantry_height": { + "default_value": 10 + }, + "machine_gcode_flavor": { + "default_value": "RepRap (Marlin/Sprinter)" + }, + "material_diameter": { + "default_value": 1.75 + }, + "material_print_temperature": { + "default_value": 210 + }, + "material_bed_temperature": { + "default_value": 55 + }, + "layer_height": { + "default_value": 0.15 + }, + "layer_height_0": { + "default_value": 0.2 + }, + "wall_thickness": { + "default_value": 1.2 + }, + "speed_print": { + "default_value": 60 + }, + "speed_infill": { + "default_value": 60 + }, + "speed_wall": { + "default_value": 30 + }, + "speed_topbottom": { + "default_value": 45 + }, + "speed_travel": { + "default_value": 125 + }, + "speed_layer_0": { + "default_value": 20 + }, + "support_enable": { + "default_value": true + }, + "retraction_enable": { + "default_value": true + }, + "retraction_amount": { + "default_value": 5 + }, + "retraction_speed": { + "default_value": 50 + } + } +} \ No newline at end of file diff --git a/resources/extruders/jgaurora_z_603s_extruder_0.def.json b/resources/extruders/jgaurora_z_603s_extruder_0.def.json new file mode 100644 index 0000000000..987425b28a --- /dev/null +++ b/resources/extruders/jgaurora_z_603s_extruder_0.def.json @@ -0,0 +1,16 @@ +{ + "id": "jgaurora_z_603s_extruder_0", + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "jgaurora_z_603s", + "position": "0" + }, + + "overrides": { + "extruder_nr": { "default_value": 0 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} From bc5f92b69b096234512fa8e9ad0f3a0d63c7dd71 Mon Sep 17 00:00:00 2001 From: pinchies <> Date: Fri, 26 Oct 2018 22:24:04 +1100 Subject: [PATCH 002/146] Add JGAurora A1 Add a preset for the JGAurora A1 to model list --- resources/definitions/jgaurora_a1.def.json | 96 +++++++++++++++++++ .../extruders/jgaurora_a1_extruder_0.def.json | 16 ++++ 2 files changed, 112 insertions(+) create mode 100644 resources/definitions/jgaurora_a1.def.json create mode 100644 resources/extruders/jgaurora_a1_extruder_0.def.json diff --git a/resources/definitions/jgaurora_a1.def.json b/resources/definitions/jgaurora_a1.def.json new file mode 100644 index 0000000000..004fd8741d --- /dev/null +++ b/resources/definitions/jgaurora_a1.def.json @@ -0,0 +1,96 @@ +{ + "name": "JGAurora A1", + "version": 2, + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Samuel Pinches", + "manufacturer": "JGAurora", + "file_formats": "text/x-gcode", + "preferred_quality_type": "fine", + "machine_extruder_trains": + { + "0": "jgaurora_a1_extruder_0" + } + }, + "overrides": { + "machine_name": { + "default_value": "JGAurora A1" + }, + "machine_start_gcode": { + "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 X0 Y0 ;move to the X/Y origin (Home)\nG28 Z0 ;move to the Z origin (Home)\nG1 Z15.0 F6000 ;move Z to position 15.0 mm as fast as possible\nG92 E0 ;zero the extruded length\nG1 X0.0 Y0.0 F1000.0 ;go to edge of print area\nG1 X60.0 E9.0 F1000.0 ;intro line\nG1 X100.0 E21.5 F1000.0 ;intro line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" + }, + "machine_end_gcode": { + "default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nM140 S0 ;turn off bed heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 ;move to the X-axis origin (Home)\nG0 Y280 F600 ;bring the bed to the front for easy print removal\nM84 ;turn off stepper motors\n; -- end of END GCODE --" + }, + "machine_width": { + "default_value": 300 + }, + "machine_height": { + "default_value": 300 + }, + "machine_depth": { + "default_value": 300 + }, + "machine_heated_bed": { + "default_value": true + }, + "machine_center_is_zero": { + "default_value": false + }, + "gantry_height": { + "default_value": 10 + }, + "machine_gcode_flavor": { + "default_value": "RepRap (Marlin/Sprinter)" + }, + "material_diameter": { + "default_value": 1.75 + }, + "material_print_temperature": { + "default_value": 215 + }, + "material_bed_temperature": { + "default_value": 67 + }, + "layer_height": { + "default_value": 0.15 + }, + "layer_height_0": { + "default_value": 0.12 + }, + "wall_thickness": { + "default_value": 1.2 + }, + "speed_print": { + "default_value": 40 + }, + "speed_infill": { + "default_value": 40 + }, + "speed_wall": { + "default_value": 35 + }, + "speed_topbottom": { + "default_value": 35 + }, + "speed_travel": { + "default_value": 120 + }, + "speed_layer_0": { + "default_value": 12 + }, + "support_enable": { + "default_value": true + }, + "retraction_enable": { + "default_value": true + }, + "retraction_amount": { + "default_value": 6 + }, + "retraction_speed": { + "default_value": 40 + } + } +} \ No newline at end of file diff --git a/resources/extruders/jgaurora_a1_extruder_0.def.json b/resources/extruders/jgaurora_a1_extruder_0.def.json new file mode 100644 index 0000000000..71742b734a --- /dev/null +++ b/resources/extruders/jgaurora_a1_extruder_0.def.json @@ -0,0 +1,16 @@ +{ + "id": "jgaurora_a1_extruder_0", + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "jgaurora_a1", + "position": "0" + }, + + "overrides": { + "extruder_nr": { "default_value": 0 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} From 5d4aa569a9d6505736944b2f5ccfdb2548d29ba7 Mon Sep 17 00:00:00 2001 From: pinchies <> Date: Fri, 26 Oct 2018 22:34:15 +1100 Subject: [PATCH 003/146] Revert "Add JGAurora A1" This reverts commit bc5f92b69b096234512fa8e9ad0f3a0d63c7dd71. --- resources/definitions/jgaurora_a1.def.json | 96 ------------------- .../extruders/jgaurora_a1_extruder_0.def.json | 16 ---- 2 files changed, 112 deletions(-) delete mode 100644 resources/definitions/jgaurora_a1.def.json delete mode 100644 resources/extruders/jgaurora_a1_extruder_0.def.json diff --git a/resources/definitions/jgaurora_a1.def.json b/resources/definitions/jgaurora_a1.def.json deleted file mode 100644 index 004fd8741d..0000000000 --- a/resources/definitions/jgaurora_a1.def.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "name": "JGAurora A1", - "version": 2, - "inherits": "fdmprinter", - "metadata": { - "visible": true, - "author": "Samuel Pinches", - "manufacturer": "JGAurora", - "file_formats": "text/x-gcode", - "preferred_quality_type": "fine", - "machine_extruder_trains": - { - "0": "jgaurora_a1_extruder_0" - } - }, - "overrides": { - "machine_name": { - "default_value": "JGAurora A1" - }, - "machine_start_gcode": { - "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 X0 Y0 ;move to the X/Y origin (Home)\nG28 Z0 ;move to the Z origin (Home)\nG1 Z15.0 F6000 ;move Z to position 15.0 mm as fast as possible\nG92 E0 ;zero the extruded length\nG1 X0.0 Y0.0 F1000.0 ;go to edge of print area\nG1 X60.0 E9.0 F1000.0 ;intro line\nG1 X100.0 E21.5 F1000.0 ;intro line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" - }, - "machine_end_gcode": { - "default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nM140 S0 ;turn off bed heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 ;move to the X-axis origin (Home)\nG0 Y280 F600 ;bring the bed to the front for easy print removal\nM84 ;turn off stepper motors\n; -- end of END GCODE --" - }, - "machine_width": { - "default_value": 300 - }, - "machine_height": { - "default_value": 300 - }, - "machine_depth": { - "default_value": 300 - }, - "machine_heated_bed": { - "default_value": true - }, - "machine_center_is_zero": { - "default_value": false - }, - "gantry_height": { - "default_value": 10 - }, - "machine_gcode_flavor": { - "default_value": "RepRap (Marlin/Sprinter)" - }, - "material_diameter": { - "default_value": 1.75 - }, - "material_print_temperature": { - "default_value": 215 - }, - "material_bed_temperature": { - "default_value": 67 - }, - "layer_height": { - "default_value": 0.15 - }, - "layer_height_0": { - "default_value": 0.12 - }, - "wall_thickness": { - "default_value": 1.2 - }, - "speed_print": { - "default_value": 40 - }, - "speed_infill": { - "default_value": 40 - }, - "speed_wall": { - "default_value": 35 - }, - "speed_topbottom": { - "default_value": 35 - }, - "speed_travel": { - "default_value": 120 - }, - "speed_layer_0": { - "default_value": 12 - }, - "support_enable": { - "default_value": true - }, - "retraction_enable": { - "default_value": true - }, - "retraction_amount": { - "default_value": 6 - }, - "retraction_speed": { - "default_value": 40 - } - } -} \ No newline at end of file diff --git a/resources/extruders/jgaurora_a1_extruder_0.def.json b/resources/extruders/jgaurora_a1_extruder_0.def.json deleted file mode 100644 index 71742b734a..0000000000 --- a/resources/extruders/jgaurora_a1_extruder_0.def.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "jgaurora_a1_extruder_0", - "version": 2, - "name": "Extruder 1", - "inherits": "fdmextruder", - "metadata": { - "machine": "jgaurora_a1", - "position": "0" - }, - - "overrides": { - "extruder_nr": { "default_value": 0 }, - "machine_nozzle_size": { "default_value": 0.4 }, - "material_diameter": { "default_value": 1.75 } - } -} From 1f97aa3df6f4330be335dc9529eede879636687e Mon Sep 17 00:00:00 2001 From: pinchies Date: Fri, 26 Oct 2018 23:37:31 +1100 Subject: [PATCH 004/146] Add files via upload --- .../extruders/jgaurora_a5_extruder_0.def.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 resources/extruders/jgaurora_a5_extruder_0.def.json diff --git a/resources/extruders/jgaurora_a5_extruder_0.def.json b/resources/extruders/jgaurora_a5_extruder_0.def.json new file mode 100644 index 0000000000..fbc6ba77e6 --- /dev/null +++ b/resources/extruders/jgaurora_a5_extruder_0.def.json @@ -0,0 +1,16 @@ +{ + "id": "jgaurora_a5_extruder_0", + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "jgaurora_a5", + "position": "0" + }, + + "overrides": { + "extruder_nr": { "default_value": 0 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} From f0277f5ef381a958e2559ffb7fbbcf33818cfb37 Mon Sep 17 00:00:00 2001 From: pinchies Date: Fri, 26 Oct 2018 23:38:30 +1100 Subject: [PATCH 005/146] Add files via upload --- resources/definitions/jgaurora_a5.def.json | 98 ++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 resources/definitions/jgaurora_a5.def.json diff --git a/resources/definitions/jgaurora_a5.def.json b/resources/definitions/jgaurora_a5.def.json new file mode 100644 index 0000000000..6143ef1523 --- /dev/null +++ b/resources/definitions/jgaurora_a5.def.json @@ -0,0 +1,98 @@ +{ + "name": "JGAurora A5", + "version": 2, + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Samuel Pinches", + "manufacturer": "JGAurora", + "file_formats": "text/x-gcode", + "platform": "jgaurora_a5.stl", + "platform_offset": [-242, -101, 273], + "preferred_quality_type": "fine", + "machine_extruder_trains": + { + "0": "jgaurora_a5_extruder_0" + } + }, + "overrides": { + "machine_name": { + "default_value": "JGAurora A5" + }, + "machine_start_gcode": { + "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 X0 Y0 ;move to the X/Y origin (Home)\nG28 Z0 ;move to the Z origin (Home)\nG1 Z15.0 F6000 ;move Z to position 15.0 mm as fast as possible\nG92 E0 ;zero the extruded length\nG1 X0.0 Y0.0 F1000.0 ;go to edge of print area\nG1 X60.0 E9.0 F1000.0 ;intro line\nG1 X100.0 E21.5 F1000.0 ;intro line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" + }, + "machine_end_gcode": { + "default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nM140 S0 ;turn off bed heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 ;move to the X-axis origin (Home)\nG0 Y280 F600 ;bring the bed to the front for easy print removal\nM84 ;turn off stepper motors\n; -- end of END GCODE --" + }, + "machine_width": { + "default_value": 300 + }, + "machine_height": { + "default_value": 320 + }, + "machine_depth": { + "default_value": 300 + }, + "machine_heated_bed": { + "default_value": true + }, + "machine_center_is_zero": { + "default_value": false + }, + "gantry_height": { + "default_value": 10 + }, + "machine_gcode_flavor": { + "default_value": "RepRap (Marlin/Sprinter)" + }, + "material_diameter": { + "default_value": 1.75 + }, + "material_print_temperature": { + "default_value": 215 + }, + "material_bed_temperature": { + "default_value": 67 + }, + "layer_height": { + "default_value": 0.15 + }, + "layer_height_0": { + "default_value": 0.12 + }, + "wall_thickness": { + "default_value": 1.2 + }, + "speed_print": { + "default_value": 40 + }, + "speed_infill": { + "default_value": 40 + }, + "speed_wall": { + "default_value": 35 + }, + "speed_topbottom": { + "default_value": 35 + }, + "speed_travel": { + "default_value": 120 + }, + "speed_layer_0": { + "default_value": 12 + }, + "support_enable": { + "default_value": true + }, + "retraction_enable": { + "default_value": true + }, + "retraction_amount": { + "default_value": 8 + }, + "retraction_speed": { + "default_value": 45 + } + } +} \ No newline at end of file From 494bedbe0d1f446c137d7705d5d66bec926760ee Mon Sep 17 00:00:00 2001 From: pinchies Date: Fri, 26 Oct 2018 23:40:27 +1100 Subject: [PATCH 006/146] Add files via upload --- resources/meshes/jgaurora_a5.stl | Bin 0 -> 1000084 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/meshes/jgaurora_a5.stl diff --git a/resources/meshes/jgaurora_a5.stl b/resources/meshes/jgaurora_a5.stl new file mode 100644 index 0000000000000000000000000000000000000000..c525b036492db15e0c2c206859e54bbf20ba5301 GIT binary patch literal 1000084 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�nAJsXQuBI;Oc_GdhnxoxS7dXRN+0{t99L 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 Spr#su3KHsGOTXJo$o~KVh0fLh literal 0 HcmV?d00001 From c8269ee4c081f312c8611222fedca71952ea7ad9 Mon Sep 17 00:00:00 2001 From: pinchies Date: Fri, 26 Oct 2018 23:42:27 +1100 Subject: [PATCH 007/146] Add files via upload --- .../extruders/jgaurora_a1_extruder_0.def.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 resources/extruders/jgaurora_a1_extruder_0.def.json diff --git a/resources/extruders/jgaurora_a1_extruder_0.def.json b/resources/extruders/jgaurora_a1_extruder_0.def.json new file mode 100644 index 0000000000..71742b734a --- /dev/null +++ b/resources/extruders/jgaurora_a1_extruder_0.def.json @@ -0,0 +1,16 @@ +{ + "id": "jgaurora_a1_extruder_0", + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "jgaurora_a1", + "position": "0" + }, + + "overrides": { + "extruder_nr": { "default_value": 0 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} From adee0fa260b861c06f174a99ea0aba1b54b26a8b Mon Sep 17 00:00:00 2001 From: pinchies Date: Fri, 26 Oct 2018 23:42:49 +1100 Subject: [PATCH 008/146] Add files via upload --- resources/definitions/jgaurora_a1.def.json | 96 ++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 resources/definitions/jgaurora_a1.def.json diff --git a/resources/definitions/jgaurora_a1.def.json b/resources/definitions/jgaurora_a1.def.json new file mode 100644 index 0000000000..004fd8741d --- /dev/null +++ b/resources/definitions/jgaurora_a1.def.json @@ -0,0 +1,96 @@ +{ + "name": "JGAurora A1", + "version": 2, + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Samuel Pinches", + "manufacturer": "JGAurora", + "file_formats": "text/x-gcode", + "preferred_quality_type": "fine", + "machine_extruder_trains": + { + "0": "jgaurora_a1_extruder_0" + } + }, + "overrides": { + "machine_name": { + "default_value": "JGAurora A1" + }, + "machine_start_gcode": { + "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 X0 Y0 ;move to the X/Y origin (Home)\nG28 Z0 ;move to the Z origin (Home)\nG1 Z15.0 F6000 ;move Z to position 15.0 mm as fast as possible\nG92 E0 ;zero the extruded length\nG1 X0.0 Y0.0 F1000.0 ;go to edge of print area\nG1 X60.0 E9.0 F1000.0 ;intro line\nG1 X100.0 E21.5 F1000.0 ;intro line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" + }, + "machine_end_gcode": { + "default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nM140 S0 ;turn off bed heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 ;move to the X-axis origin (Home)\nG0 Y280 F600 ;bring the bed to the front for easy print removal\nM84 ;turn off stepper motors\n; -- end of END GCODE --" + }, + "machine_width": { + "default_value": 300 + }, + "machine_height": { + "default_value": 300 + }, + "machine_depth": { + "default_value": 300 + }, + "machine_heated_bed": { + "default_value": true + }, + "machine_center_is_zero": { + "default_value": false + }, + "gantry_height": { + "default_value": 10 + }, + "machine_gcode_flavor": { + "default_value": "RepRap (Marlin/Sprinter)" + }, + "material_diameter": { + "default_value": 1.75 + }, + "material_print_temperature": { + "default_value": 215 + }, + "material_bed_temperature": { + "default_value": 67 + }, + "layer_height": { + "default_value": 0.15 + }, + "layer_height_0": { + "default_value": 0.12 + }, + "wall_thickness": { + "default_value": 1.2 + }, + "speed_print": { + "default_value": 40 + }, + "speed_infill": { + "default_value": 40 + }, + "speed_wall": { + "default_value": 35 + }, + "speed_topbottom": { + "default_value": 35 + }, + "speed_travel": { + "default_value": 120 + }, + "speed_layer_0": { + "default_value": 12 + }, + "support_enable": { + "default_value": true + }, + "retraction_enable": { + "default_value": true + }, + "retraction_amount": { + "default_value": 6 + }, + "retraction_speed": { + "default_value": 40 + } + } +} \ No newline at end of file From 79ef3635a56ad76ac77816f98d1216b999c33d62 Mon Sep 17 00:00:00 2001 From: pinchies Date: Fri, 26 Oct 2018 23:45:32 +1100 Subject: [PATCH 009/146] Add files via upload --- resources/extruders/alfawise_u20.def.json | 96 +++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 resources/extruders/alfawise_u20.def.json diff --git a/resources/extruders/alfawise_u20.def.json b/resources/extruders/alfawise_u20.def.json new file mode 100644 index 0000000000..f6dccce3ee --- /dev/null +++ b/resources/extruders/alfawise_u20.def.json @@ -0,0 +1,96 @@ +{ + "name": "Alfawise U20", + "version": 2, + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Samuel Pinches", + "manufacturer": "Alfawise", + "file_formats": "text/x-gcode", + "preferred_quality_type": "fine", + "machine_extruder_trains": + { + "0": "alfawise_u20_extruder_0" + } + }, + "overrides": { + "machine_name": { + "default_value": "Alfawise U20" + }, + "machine_start_gcode": { + "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 X0 Y0 ;move to the X/Y origin (Home)\nG28 Z0 ;move to the Z origin (Home)\nG1 Z15.0 F6000 ;move Z to position 15.0 mm as fast as possible\nG92 E0 ;zero the extruded length\nG1 X0.0 Y0.0 F1000.0 ;go to edge of print area\nG1 X60.0 E9.0 F1000.0 ;intro line\nG1 X100.0 E21.5 F1000.0 ;intro line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" + }, + "machine_end_gcode": { + "default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nM140 S0 ;turn off bed heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 ;move to the X-axis origin (Home)\nG0 Y280 F600 ;bring the bed to the front for easy print removal\nM84 ;turn off stepper motors\n; -- end of END GCODE --" + }, + "machine_width": { + "default_value": 300 + }, + "machine_height": { + "default_value": 400 + }, + "machine_depth": { + "default_value": 300 + }, + "machine_heated_bed": { + "default_value": true + }, + "machine_center_is_zero": { + "default_value": false + }, + "gantry_height": { + "default_value": 10 + }, + "machine_gcode_flavor": { + "default_value": "RepRap (Marlin/Sprinter)" + }, + "material_diameter": { + "default_value": 1.75 + }, + "material_print_temperature": { + "default_value": 210 + }, + "material_bed_temperature": { + "default_value": 50 + }, + "layer_height": { + "default_value": 0.15 + }, + "layer_height_0": { + "default_value": 0.2 + }, + "wall_thickness": { + "default_value": 1.2 + }, + "speed_print": { + "default_value": 40 + }, + "speed_infill": { + "default_value": 40 + }, + "speed_wall": { + "default_value": 35 + }, + "speed_topbottom": { + "default_value": 35 + }, + "speed_travel": { + "default_value": 120 + }, + "speed_layer_0": { + "default_value": 20 + }, + "support_enable": { + "default_value": true + }, + "retraction_enable": { + "default_value": true + }, + "retraction_amount": { + "default_value": 5 + }, + "retraction_speed": { + "default_value": 45 + } + } +} \ No newline at end of file From 03532969c6c17883750f3bafb76e0e82f319cfbd Mon Sep 17 00:00:00 2001 From: pinchies Date: Fri, 26 Oct 2018 23:46:39 +1100 Subject: [PATCH 010/146] Add files via upload --- resources/definitions/alfawise_u20.def.json | 96 +++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 resources/definitions/alfawise_u20.def.json diff --git a/resources/definitions/alfawise_u20.def.json b/resources/definitions/alfawise_u20.def.json new file mode 100644 index 0000000000..f6dccce3ee --- /dev/null +++ b/resources/definitions/alfawise_u20.def.json @@ -0,0 +1,96 @@ +{ + "name": "Alfawise U20", + "version": 2, + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Samuel Pinches", + "manufacturer": "Alfawise", + "file_formats": "text/x-gcode", + "preferred_quality_type": "fine", + "machine_extruder_trains": + { + "0": "alfawise_u20_extruder_0" + } + }, + "overrides": { + "machine_name": { + "default_value": "Alfawise U20" + }, + "machine_start_gcode": { + "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 X0 Y0 ;move to the X/Y origin (Home)\nG28 Z0 ;move to the Z origin (Home)\nG1 Z15.0 F6000 ;move Z to position 15.0 mm as fast as possible\nG92 E0 ;zero the extruded length\nG1 X0.0 Y0.0 F1000.0 ;go to edge of print area\nG1 X60.0 E9.0 F1000.0 ;intro line\nG1 X100.0 E21.5 F1000.0 ;intro line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" + }, + "machine_end_gcode": { + "default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nM140 S0 ;turn off bed heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 ;move to the X-axis origin (Home)\nG0 Y280 F600 ;bring the bed to the front for easy print removal\nM84 ;turn off stepper motors\n; -- end of END GCODE --" + }, + "machine_width": { + "default_value": 300 + }, + "machine_height": { + "default_value": 400 + }, + "machine_depth": { + "default_value": 300 + }, + "machine_heated_bed": { + "default_value": true + }, + "machine_center_is_zero": { + "default_value": false + }, + "gantry_height": { + "default_value": 10 + }, + "machine_gcode_flavor": { + "default_value": "RepRap (Marlin/Sprinter)" + }, + "material_diameter": { + "default_value": 1.75 + }, + "material_print_temperature": { + "default_value": 210 + }, + "material_bed_temperature": { + "default_value": 50 + }, + "layer_height": { + "default_value": 0.15 + }, + "layer_height_0": { + "default_value": 0.2 + }, + "wall_thickness": { + "default_value": 1.2 + }, + "speed_print": { + "default_value": 40 + }, + "speed_infill": { + "default_value": 40 + }, + "speed_wall": { + "default_value": 35 + }, + "speed_topbottom": { + "default_value": 35 + }, + "speed_travel": { + "default_value": 120 + }, + "speed_layer_0": { + "default_value": 20 + }, + "support_enable": { + "default_value": true + }, + "retraction_enable": { + "default_value": true + }, + "retraction_amount": { + "default_value": 5 + }, + "retraction_speed": { + "default_value": 45 + } + } +} \ No newline at end of file From 135ad6d7054a84591a635ee4c4c21945dfac44be Mon Sep 17 00:00:00 2001 From: pinchies Date: Fri, 26 Oct 2018 23:53:59 +1100 Subject: [PATCH 011/146] Add files via upload --- .../cocoon_create_modelmaker_extruder_0.def.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 resources/extruders/cocoon_create_modelmaker_extruder_0.def.json diff --git a/resources/extruders/cocoon_create_modelmaker_extruder_0.def.json b/resources/extruders/cocoon_create_modelmaker_extruder_0.def.json new file mode 100644 index 0000000000..26d847483d --- /dev/null +++ b/resources/extruders/cocoon_create_modelmaker_extruder_0.def.json @@ -0,0 +1,16 @@ +{ + "id": "cocoon_create_modelmaker_extruder_0", + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "cocoon_create_modelmaker", + "position": "0" + }, + + "overrides": { + "extruder_nr": { "default_value": 0 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} From 49ac4233839e80423767ce13883cd3f9ba7a0cc5 Mon Sep 17 00:00:00 2001 From: pinchies Date: Fri, 26 Oct 2018 23:54:29 +1100 Subject: [PATCH 012/146] Add files via upload --- .../cocoon_create_modelmaker.def.json | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 resources/definitions/cocoon_create_modelmaker.def.json diff --git a/resources/definitions/cocoon_create_modelmaker.def.json b/resources/definitions/cocoon_create_modelmaker.def.json new file mode 100644 index 0000000000..f752a08fc7 --- /dev/null +++ b/resources/definitions/cocoon_create_modelmaker.def.json @@ -0,0 +1,96 @@ +{ + "name": "Cocoon Create ModelMaker", + "version": 2, + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Samuel Pinches", + "manufacturer": "Cocoon Create", + "file_formats": "text/x-gcode", + "preferred_quality_type": "fine", + "machine_extruder_trains": + { + "0": "cocoon_create_modelmaker_extruder_0" + } + }, + "overrides": { + "machine_name": { + "default_value": "Cocoon Create ModelMaker" + }, + "machine_start_gcode": { + "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 X0 Y0 ;move to the X/Y origin (Home)\nG28 Z0 ;move to the Z origin (Home)\nG1 Z15.0 F6000 ;move Z to position 15.0 mm as fast as possible\nG92 E0 ;zero the extruded length\nG1 X0.0 Y0.0 F1000.0 ;go to edge of print area\nG1 X60.0 E9.0 F1000.0 ;intro line\nG1 X100.0 E21.5 F1000.0 ;intro line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" + }, + "machine_end_gcode": { + "default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 Y0 ;move to the XY-axis origin (Home)\nM84 ;turn off stepper motors\n; -- end of END GCODE --" + }, + "machine_width": { + "default_value": 120 + }, + "machine_height": { + "default_value": 100 + }, + "machine_depth": { + "default_value": 135 + }, + "machine_heated_bed": { + "default_value": false + }, + "machine_center_is_zero": { + "default_value": false + }, + "gantry_height": { + "default_value": 10 + }, + "machine_gcode_flavor": { + "default_value": "RepRap (Marlin/Sprinter)" + }, + "material_diameter": { + "default_value": 1.75 + }, + "material_print_temperature": { + "default_value": 220 + }, + "layer_height": { + "default_value": 0.15 + }, + "layer_height_0": { + "default_value": 0.2 + }, + "wall_thickness": { + "default_value": 1.2 + }, + "top_bottom_thickness": { + "default_value": 0.6 + }, + "speed_print": { + "default_value": 40 + }, + "speed_infill": { + "default_value": 40 + }, + "speed_wall": { + "default_value": 35 + }, + "speed_topbottom": { + "default_value": 35 + }, + "speed_travel": { + "default_value": 70 + }, + "speed_layer_0": { + "default_value": 20 + }, + "support_enable": { + "default_value": true + }, + "retraction_enable": { + "default_value": true + }, + "retraction_amount": { + "default_value": 7 + }, + "retraction_speed": { + "default_value": 40 + } + } +} \ No newline at end of file From 5a759fa9f493bb034170b0cec566653b22b51793 Mon Sep 17 00:00:00 2001 From: pinchies Date: Sat, 27 Oct 2018 03:47:29 +1100 Subject: [PATCH 013/146] fix start routing --- resources/definitions/jgaurora_a5.def.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/jgaurora_a5.def.json b/resources/definitions/jgaurora_a5.def.json index 6143ef1523..0a8b93f7cf 100644 --- a/resources/definitions/jgaurora_a5.def.json +++ b/resources/definitions/jgaurora_a5.def.json @@ -20,7 +20,7 @@ "default_value": "JGAurora A5" }, "machine_start_gcode": { - "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 X0 Y0 ;move to the X/Y origin (Home)\nG28 Z0 ;move to the Z origin (Home)\nG1 Z15.0 F6000 ;move Z to position 15.0 mm as fast as possible\nG92 E0 ;zero the extruded length\nG1 X0.0 Y0.0 F1000.0 ;go to edge of print area\nG1 X60.0 E9.0 F1000.0 ;intro line\nG1 X100.0 E21.5 F1000.0 ;intro line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" + "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 ;home all axis\nM420 S1 ;turn on mesh bed levelling if enabled in firmware\nG92 E0 ;zero the extruded length\nG1 Z1 F1000 ;move up slightly\nG1 X60.0 Z0 E9.0 F1000.0;intro line\nG1 X100.0 E21.5 F1000.0 ;continue line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" }, "machine_end_gcode": { "default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nM140 S0 ;turn off bed heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 ;move to the X-axis origin (Home)\nG0 Y280 F600 ;bring the bed to the front for easy print removal\nM84 ;turn off stepper motors\n; -- end of END GCODE --" @@ -95,4 +95,4 @@ "default_value": 45 } } -} \ No newline at end of file +} From 6fa382a527137024d84a4cdf169cfc8a7dce67bb Mon Sep 17 00:00:00 2001 From: pinchies Date: Sat, 27 Oct 2018 03:48:24 +1100 Subject: [PATCH 014/146] Update jgaurora_a1.def.json --- resources/definitions/jgaurora_a1.def.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/jgaurora_a1.def.json b/resources/definitions/jgaurora_a1.def.json index 004fd8741d..4fd2eb4994 100644 --- a/resources/definitions/jgaurora_a1.def.json +++ b/resources/definitions/jgaurora_a1.def.json @@ -18,7 +18,7 @@ "default_value": "JGAurora A1" }, "machine_start_gcode": { - "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 X0 Y0 ;move to the X/Y origin (Home)\nG28 Z0 ;move to the Z origin (Home)\nG1 Z15.0 F6000 ;move Z to position 15.0 mm as fast as possible\nG92 E0 ;zero the extruded length\nG1 X0.0 Y0.0 F1000.0 ;go to edge of print area\nG1 X60.0 E9.0 F1000.0 ;intro line\nG1 X100.0 E21.5 F1000.0 ;intro line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" + "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 ;home all axis\nM420 S1 ;turn on mesh bed levelling if enabled in firmware\nG92 E0 ;zero the extruded length\nG1 Z1 F1000 ;move up slightly\nG1 X60.0 Z0 E9.0 F1000.0;intro line\nG1 X100.0 E21.5 F1000.0 ;continue line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" }, "machine_end_gcode": { "default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nM140 S0 ;turn off bed heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 ;move to the X-axis origin (Home)\nG0 Y280 F600 ;bring the bed to the front for easy print removal\nM84 ;turn off stepper motors\n; -- end of END GCODE --" @@ -93,4 +93,4 @@ "default_value": 40 } } -} \ No newline at end of file +} From 0487288edfaf3c10ae69112ab393e54eef4d01df Mon Sep 17 00:00:00 2001 From: pinchies Date: Sat, 27 Oct 2018 03:49:34 +1100 Subject: [PATCH 015/146] Update alfawise_u20.def.json --- resources/definitions/alfawise_u20.def.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/alfawise_u20.def.json b/resources/definitions/alfawise_u20.def.json index f6dccce3ee..abc206c4d2 100644 --- a/resources/definitions/alfawise_u20.def.json +++ b/resources/definitions/alfawise_u20.def.json @@ -18,7 +18,7 @@ "default_value": "Alfawise U20" }, "machine_start_gcode": { - "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 X0 Y0 ;move to the X/Y origin (Home)\nG28 Z0 ;move to the Z origin (Home)\nG1 Z15.0 F6000 ;move Z to position 15.0 mm as fast as possible\nG92 E0 ;zero the extruded length\nG1 X0.0 Y0.0 F1000.0 ;go to edge of print area\nG1 X60.0 E9.0 F1000.0 ;intro line\nG1 X100.0 E21.5 F1000.0 ;intro line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" + "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 ;home all axis\nG92 E0 ;zero the extruded length\nG1 Z1 F1000 ;move up slightly\nG1 X60.0 Z0 E9.0 F1000.0;intro line\nG1 X100.0 E21.5 F1000.0 ;continue line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" }, "machine_end_gcode": { "default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nM140 S0 ;turn off bed heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 ;move to the X-axis origin (Home)\nG0 Y280 F600 ;bring the bed to the front for easy print removal\nM84 ;turn off stepper motors\n; -- end of END GCODE --" @@ -93,4 +93,4 @@ "default_value": 45 } } -} \ No newline at end of file +} From 46c6ef71abfcdb6d617c724e3f72af77a5e48176 Mon Sep 17 00:00:00 2001 From: pinchies Date: Sat, 27 Oct 2018 03:50:15 +1100 Subject: [PATCH 016/146] Update jgaurora_z_603s.def.json --- resources/definitions/jgaurora_z_603s.def.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/jgaurora_z_603s.def.json b/resources/definitions/jgaurora_z_603s.def.json index af7fa823db..59e0ff129c 100644 --- a/resources/definitions/jgaurora_z_603s.def.json +++ b/resources/definitions/jgaurora_z_603s.def.json @@ -18,7 +18,7 @@ "default_value": "JGAurora Z-603S" }, "machine_start_gcode": { - "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 X0 Y0 ;move to the X/Y origin (Home)\nG28 Z0 ;move to the Z origin (Home)\nG1 Z15.0 F6000 ;move Z to position 15.0 mm as fast as possible\nG92 E0 ;zero the extruded length\nG1 X0.0 Y0.0 F1000.0 ;go to edge of print area\nG1 X60.0 E9.0 F1000.0 ;intro line\nG1 X100.0 E21.5 F1000.0 ;intro line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" + "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 ;home all axis\nM420 S1 ;turn on mesh bed levelling if enabled in firmware\nG92 E0 ;zero the extruded length\nG1 Z1 F1000 ;move up slightly\nG1 X60.0 Z0 E9.0 F1000.0;intro line\nG1 X100.0 E21.5 F1000.0 ;continue line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" }, "machine_end_gcode": { "default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nM140 S0 ;turn off bed heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 ;move to the X-axis origin (Home)\nG0 Y280 F600 ;bring the bed to the front for easy print removal\nM84 ;turn off stepper motors\n; -- end of END GCODE --" @@ -93,4 +93,4 @@ "default_value": 50 } } -} \ No newline at end of file +} From cdb5ab193051b4fb16752aeb57fe386c9a3d4e59 Mon Sep 17 00:00:00 2001 From: pinchies Date: Sat, 27 Oct 2018 03:51:05 +1100 Subject: [PATCH 017/146] Update cocoon_create_modelmaker.def.json --- resources/definitions/cocoon_create_modelmaker.def.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/cocoon_create_modelmaker.def.json b/resources/definitions/cocoon_create_modelmaker.def.json index f752a08fc7..7f19fb0239 100644 --- a/resources/definitions/cocoon_create_modelmaker.def.json +++ b/resources/definitions/cocoon_create_modelmaker.def.json @@ -18,7 +18,7 @@ "default_value": "Cocoon Create ModelMaker" }, "machine_start_gcode": { - "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 X0 Y0 ;move to the X/Y origin (Home)\nG28 Z0 ;move to the Z origin (Home)\nG1 Z15.0 F6000 ;move Z to position 15.0 mm as fast as possible\nG92 E0 ;zero the extruded length\nG1 X0.0 Y0.0 F1000.0 ;go to edge of print area\nG1 X60.0 E9.0 F1000.0 ;intro line\nG1 X100.0 E21.5 F1000.0 ;intro line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" + "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 ;home all axis\nG92 E0 ;zero the extruded length\nG1 Z1 F1000 ;move up slightly\nG1 X60.0 Z0 E9.0 F1000.0;intro line\nG1 X100.0 E21.5 F1000.0 ;continue line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" }, "machine_end_gcode": { "default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 Y0 ;move to the XY-axis origin (Home)\nM84 ;turn off stepper motors\n; -- end of END GCODE --" @@ -93,4 +93,4 @@ "default_value": 40 } } -} \ No newline at end of file +} From 34b76596f19931cd7498c837e105d95f6db1d445 Mon Sep 17 00:00:00 2001 From: Shelby Merrick Date: Fri, 2 Nov 2018 09:39:12 -0400 Subject: [PATCH 018/146] Updated to reference speed_travel macro --- resources/definitions/wanhao_d4s.def.json | 4 ++-- resources/definitions/wanhao_d6.def.json | 4 ++-- resources/definitions/wanhao_d6_plus.def.json | 4 ++-- resources/definitions/wanhao_duplicator5S.def.json | 4 ++-- resources/definitions/wanhao_duplicator5Smini.def.json | 4 ++-- resources/definitions/wanhao_i3.def.json | 4 ++-- resources/definitions/wanhao_i3mini.def.json | 4 ++-- resources/definitions/wanhao_i3plus.def.json | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/resources/definitions/wanhao_d4s.def.json b/resources/definitions/wanhao_d4s.def.json index 8788353e92..c1807923c6 100644 --- a/resources/definitions/wanhao_d4s.def.json +++ b/resources/definitions/wanhao_d4s.def.json @@ -39,10 +39,10 @@ "default_value": "RepRap (Marlin/Sprinter)" }, "machine_start_gcode": { - "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{travel_speed} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{travel_speed} \n ;Put printing message on LCD screen\n M117 Printing..." + "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{speed_travel} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{speed_travel} \n ;Put printing message on LCD screen\n M117 Printing..." }, "machine_end_gcode": { - "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning" + "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{speed_travel} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning" } } } diff --git a/resources/definitions/wanhao_d6.def.json b/resources/definitions/wanhao_d6.def.json index 7ca3031124..c8a690d02c 100644 --- a/resources/definitions/wanhao_d6.def.json +++ b/resources/definitions/wanhao_d6.def.json @@ -42,10 +42,10 @@ "default_value": "RepRap (Marlin/Sprinter)" }, "machine_start_gcode": { - "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{travel_speed} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{travel_speed} \n ;Put printing message on LCD screen\n M117 Printing..." + "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{speed_travel} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{speed_travel} \n ;Put printing message on LCD screen\n M117 Printing..." }, "machine_end_gcode": { - "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning" + "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{speed_travel} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning" } } } diff --git a/resources/definitions/wanhao_d6_plus.def.json b/resources/definitions/wanhao_d6_plus.def.json index f17b58db85..b3b5ed9b0a 100644 --- a/resources/definitions/wanhao_d6_plus.def.json +++ b/resources/definitions/wanhao_d6_plus.def.json @@ -39,10 +39,10 @@ "default_value": "RepRap (Marlin/Sprinter)" }, "machine_start_gcode": { - "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{travel_speed} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{travel_speed} \n ;Put printing message on LCD screen\n M117 Printing..." + "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{speed_travel} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{speed_travel} \n ;Put printing message on LCD screen\n M117 Printing..." }, "machine_end_gcode": { - "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning" + "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{speed_travel} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning" } } } diff --git a/resources/definitions/wanhao_duplicator5S.def.json b/resources/definitions/wanhao_duplicator5S.def.json index 1d29b90249..b27a13fda8 100644 --- a/resources/definitions/wanhao_duplicator5S.def.json +++ b/resources/definitions/wanhao_duplicator5S.def.json @@ -42,10 +42,10 @@ "default_value": "RepRap (Marlin/Sprinter)" }, "machine_start_gcode": { - "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{travel_speed} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{travel_speed} \n ;Put printing message on LCD screen\n M117 Printing..." + "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{speed_travel} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{speed_travel} \n ;Put printing message on LCD screen\n M117 Printing..." }, "machine_end_gcode": { - "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning" + "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{speed_travel} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning" } } } diff --git a/resources/definitions/wanhao_duplicator5Smini.def.json b/resources/definitions/wanhao_duplicator5Smini.def.json index e7f9359cf1..e3ef0b92fe 100644 --- a/resources/definitions/wanhao_duplicator5Smini.def.json +++ b/resources/definitions/wanhao_duplicator5Smini.def.json @@ -39,10 +39,10 @@ "default_value": "RepRap (Marlin/Sprinter)" }, "machine_start_gcode": { - "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{travel_speed} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{travel_speed} \n ;Put printing message on LCD screen\n M117 Printing..." + "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{speed_travel} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{speed_travel} \n ;Put printing message on LCD screen\n M117 Printing..." }, "machine_end_gcode": { - "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning" + "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{speed_travel} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning" } } } diff --git a/resources/definitions/wanhao_i3.def.json b/resources/definitions/wanhao_i3.def.json index 15121f8b8b..42b19c8748 100644 --- a/resources/definitions/wanhao_i3.def.json +++ b/resources/definitions/wanhao_i3.def.json @@ -39,10 +39,10 @@ "default_value": "RepRap (Marlin/Sprinter)" }, "machine_start_gcode": { - "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{travel_speed} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{travel_speed} \n ;Put printing message on LCD screen\n M117 Printing..." + "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{speed_travel} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{speed_travel} \n ;Put printing message on LCD screen\n M117 Printing..." }, "machine_end_gcode": { - "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning" + "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{speed_travel} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning" } } } diff --git a/resources/definitions/wanhao_i3mini.def.json b/resources/definitions/wanhao_i3mini.def.json index 057fca81a6..0c70391c27 100644 --- a/resources/definitions/wanhao_i3mini.def.json +++ b/resources/definitions/wanhao_i3mini.def.json @@ -39,10 +39,10 @@ "default_value": "RepRap (Marlin/Sprinter)" }, "machine_start_gcode": { - "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{travel_speed} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{travel_speed} \n ;Put printing message on LCD screen\n M117 Printing..." + "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{speed_travel} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{speed_travel} \n ;Put printing message on LCD screen\n M117 Printing..." }, "machine_end_gcode": { - "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning" + "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{speed_travel} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning" } } } diff --git a/resources/definitions/wanhao_i3plus.def.json b/resources/definitions/wanhao_i3plus.def.json index 2b705c6ff5..e454a40ae1 100644 --- a/resources/definitions/wanhao_i3plus.def.json +++ b/resources/definitions/wanhao_i3plus.def.json @@ -39,10 +39,10 @@ "default_value": "RepRap (Marlin/Sprinter)" }, "machine_start_gcode": { - "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{travel_speed} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{travel_speed} \n ;Put printing message on LCD screen\n M117 Printing..." + "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{speed_travel} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{speed_travel} \n ;Put printing message on LCD screen\n M117 Printing..." }, "machine_end_gcode": { - "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning" + "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{speed_travel} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning" } } } From c51995f3497dba6880014f45e077f0dd494329b1 Mon Sep 17 00:00:00 2001 From: pinchies Date: Wed, 7 Nov 2018 04:36:32 +1100 Subject: [PATCH 019/146] Update jgaurora_a5.def.json --- resources/definitions/jgaurora_a5.def.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/jgaurora_a5.def.json b/resources/definitions/jgaurora_a5.def.json index 0a8b93f7cf..02d9a9db4f 100644 --- a/resources/definitions/jgaurora_a5.def.json +++ b/resources/definitions/jgaurora_a5.def.json @@ -1,5 +1,5 @@ { - "name": "JGAurora A5", + "name": "JGAurora A5 & A5S", "version": 2, "inherits": "fdmprinter", "metadata": { @@ -17,7 +17,7 @@ }, "overrides": { "machine_name": { - "default_value": "JGAurora A5" + "default_value": "JGAurora A5 & A5S" }, "machine_start_gcode": { "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 ;home all axis\nM420 S1 ;turn on mesh bed levelling if enabled in firmware\nG92 E0 ;zero the extruded length\nG1 Z1 F1000 ;move up slightly\nG1 X60.0 Z0 E9.0 F1000.0;intro line\nG1 X100.0 E21.5 F1000.0 ;continue line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" From 70d2f407a37f186fe3712042ac84d67da1c75e12 Mon Sep 17 00:00:00 2001 From: pinchies Date: Wed, 7 Nov 2018 04:38:39 +1100 Subject: [PATCH 020/146] Update cocoon_create_modelmaker.def.json --- resources/definitions/cocoon_create_modelmaker.def.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/definitions/cocoon_create_modelmaker.def.json b/resources/definitions/cocoon_create_modelmaker.def.json index 7f19fb0239..204d5b9492 100644 --- a/resources/definitions/cocoon_create_modelmaker.def.json +++ b/resources/definitions/cocoon_create_modelmaker.def.json @@ -1,11 +1,11 @@ { - "name": "Cocoon Create ModelMaker", + "name": "Cocoon Create ModelMaker & Wanhao Duplicator i3 Mini", "version": 2, "inherits": "fdmprinter", "metadata": { "visible": true, "author": "Samuel Pinches", - "manufacturer": "Cocoon Create", + "manufacturer": "Cocoon Create / Wanhao", "file_formats": "text/x-gcode", "preferred_quality_type": "fine", "machine_extruder_trains": @@ -15,7 +15,7 @@ }, "overrides": { "machine_name": { - "default_value": "Cocoon Create ModelMaker" + "default_value": "Cocoon Create ModelMaker & Wanhao Duplicator i3 Mini" }, "machine_start_gcode": { "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 ;home all axis\nG92 E0 ;zero the extruded length\nG1 Z1 F1000 ;move up slightly\nG1 X60.0 Z0 E9.0 F1000.0;intro line\nG1 X100.0 E21.5 F1000.0 ;continue line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" From e9e8c49b2d9d0c898c88dced34b8179b8bc1f0de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijn=20De=C3=A9?= Date: Fri, 16 Nov 2018 14:16:45 +0100 Subject: [PATCH 021/146] Added tests for SendMaterialJob and refactored SendMaterialJob for better testability. This is part of a larger project to create tests for the UM3NetworkPrinting plugin in preparation for printing from the cloud --- plugins/UM3NetworkPrinting/src/Models.py | 87 +++++ .../UM3NetworkPrinting/src/SendMaterialJob.py | 124 ++++--- .../UM3NetworkPrinting/TestSendMaterialJob.py | 327 ++++++++++++++++++ 3 files changed, 491 insertions(+), 47 deletions(-) create mode 100644 plugins/UM3NetworkPrinting/src/Models.py create mode 100644 tests/plugins/UM3NetworkPrinting/TestSendMaterialJob.py diff --git a/plugins/UM3NetworkPrinting/src/Models.py b/plugins/UM3NetworkPrinting/src/Models.py new file mode 100644 index 0000000000..6d42b39370 --- /dev/null +++ b/plugins/UM3NetworkPrinting/src/Models.py @@ -0,0 +1,87 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from typing import Optional + + +class BaseModel: + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + def __eq__(self, other): + return self.__dict__ == other.__dict__ if type(self) == type(other) else False + + +## Represents an item in the cluster API response for installed materials. +class ClusterMaterial(BaseModel): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.version = int(self.version) + self.density = float(self.density) + + guid: None # type: Optional[str] + + material: None # type: Optional[str] + + brand: None # type: Optional[str] + + version = None # type: Optional[int] + + color: None # type: Optional[str] + + density: None # type: Optional[float] + + +class LocalMaterialProperties(BaseModel): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.density = float(self.density) + self.diameter = float(self.diameter) + self.weight = float(self.weight) + + density: None # type: Optional[float] + + diameter: None # type: Optional[float] + + weight: None # type: Optional[int] + + +class LocalMaterial(BaseModel): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.properties = LocalMaterialProperties(**self.properties) + self.approximate_diameter = float(self.approximate_diameter) + self.version = int(self.version) + + GUID: None # type: Optional[str] + + id: None # type: Optional[str] + + type: None # type: Optional[str] + + status: None # type: Optional[str] + + base_file: None # type: Optional[str] + + setting_version: None # type: Optional[str] + + version = None # type: Optional[int] + + name: None # type: Optional[str] + + brand: None # type: Optional[str] + + material: None # type: Optional[str] + + color_name: None # type: Optional[str] + + description: None # type: Optional[str] + + adhesion_info: None # type: Optional[str] + + approximate_diameter: None # type: Optional[float] + + properties: None # type: LocalMaterialProperties + + definition: None # type: Optional[str] + + compatible: None # type: Optional[bool] diff --git a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py index 8491e79c29..126ed07317 100644 --- a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py @@ -1,99 +1,129 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -import json #To understand the list of materials from the printer reply. -import os #To walk over material files. -import os.path #To filter on material files. -from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest #To listen to the reply from the printer. -from typing import Any, Dict, Set, TYPE_CHECKING -import urllib.parse #For getting material IDs from their file names. +import json # To understand the list of materials from the printer reply. +import os # To walk over material files. +import os.path # To filter on material files. +import urllib.parse # For getting material IDs from their file names. +from typing import Dict, TYPE_CHECKING -from UM.Job import Job #The interface we're implementing. +from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest # To listen to the reply from the printer. + +from UM.Job import Job # The interface we're implementing. from UM.Logger import Logger -from UM.MimeTypeDatabase import MimeTypeDatabase #To strip the extensions of the material profile files. +from UM.MimeTypeDatabase import MimeTypeDatabase # To strip the extensions of the material profile files. from UM.Resources import Resources -from UM.Settings.ContainerRegistry import ContainerRegistry #To find the GUIDs of materials. - -from cura.CuraApplication import CuraApplication #For the resource types. +from UM.Settings.ContainerRegistry import ContainerRegistry # To find the GUIDs of materials. +from cura.CuraApplication import CuraApplication # For the resource types. +from plugins.UM3NetworkPrinting.src.Models import ClusterMaterial, LocalMaterial if TYPE_CHECKING: from .ClusterUM3OutputDevice import ClusterUM3OutputDevice + ## Asynchronous job to send material profiles to the printer. # # This way it won't freeze up the interface while sending those materials. class SendMaterialJob(Job): def __init__(self, device: "ClusterUM3OutputDevice") -> None: super().__init__() - self.device = device #type: ClusterUM3OutputDevice + self.device = device # type: ClusterUM3OutputDevice def run(self) -> None: - self.device.get("materials/", on_finished = self.sendMissingMaterials) + self.device.get("materials/", on_finished=self.sendMissingMaterials) def sendMissingMaterials(self, reply: QNetworkReply) -> None: - if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: #Got an error from the HTTP request. + if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: # Got an error from the HTTP request. Logger.log("e", "Couldn't request current material storage on printer. Not syncing materials.") return - remote_materials_list = reply.readAll().data().decode("utf-8") + # Collect materials from the printer's reply try: - remote_materials_list = json.loads(remote_materials_list) + remote_materials_by_guid = self._parseReply(reply) except json.JSONDecodeError: Logger.log("e", "Request material storage on printer: I didn't understand the printer's answer.") return - try: - remote_materials_by_guid = {material["guid"]: material for material in remote_materials_list} #Index by GUID. except KeyError: Logger.log("e", "Request material storage on printer: Printer's answer was missing GUIDs.") return - container_registry = ContainerRegistry.getInstance() - local_materials_list = filter(lambda material: ("GUID" in material and "version" in material and "id" in material), container_registry.findContainersMetadata(type = "material")) - local_materials_by_guid = {material["GUID"]: material for material in local_materials_list if material["id"] == material["base_file"]} - for material in local_materials_list: #For each GUID get the material with the highest version number. - try: - if int(material["version"]) > local_materials_by_guid[material["GUID"]]["version"]: - local_materials_by_guid[material["GUID"]] = material - except ValueError: - Logger.log("e", "Material {material_id} has invalid version number {number}.".format(material_id = material["id"], number = material["version"])) - continue + # Collect local materials + local_materials_by_guid = self._getLocalMaterials() - materials_to_send = set() #type: Set[Dict[str, Any]] - for guid, material in local_materials_by_guid.items(): - if guid not in remote_materials_by_guid: - materials_to_send.add(material["id"]) - continue - try: - if int(material["version"]) > remote_materials_by_guid[guid]["version"]: - materials_to_send.add(material["id"]) - continue - except KeyError: - Logger.log("e", "Current material storage on printer was an invalid reply (missing version).") - return + # Find out what materials are new or updated annd must be sent to the printer + materials_to_send = { + material.id + for guid, material in local_materials_by_guid.items() + if guid not in remote_materials_by_guid or + material.version > remote_materials_by_guid[guid].version + } + # Send materials to the printer + self.sendMaterialsToPrinter(materials_to_send) + + def sendMaterialsToPrinter(self, materials_to_send): for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.MaterialInstanceContainer): try: mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path) except MimeTypeDatabase.MimeTypeNotFoundError: - continue #Not the sort of file we'd like to send then. + continue # Not the sort of file we'd like to send then. + _, file_name = os.path.split(file_path) material_id = urllib.parse.unquote_plus(mime_type.stripExtension(file_name)) + if material_id not in materials_to_send: continue parts = [] with open(file_path, "rb") as f: - parts.append(self.device._createFormPart("name=\"file\"; filename=\"{file_name}\"".format(file_name = file_name), f.read())) + parts.append( + self.device._createFormPart("name=\"file\"; filename=\"{file_name}\"".format(file_name=file_name), + f.read())) signature_file_path = file_path + ".sig" if os.path.exists(signature_file_path): _, signature_file_name = os.path.split(signature_file_path) with open(signature_file_path, "rb") as f: - parts.append(self.device._createFormPart("name=\"signature_file\"; filename=\"{file_name}\"".format(file_name = signature_file_name), f.read())) + parts.append(self.device._createFormPart( + "name=\"signature_file\"; filename=\"{file_name}\"".format(file_name=signature_file_name), + f.read())) - Logger.log("d", "Syncing material {material_id} with cluster.".format(material_id = material_id)) - self.device.postFormWithParts(target = "materials/", parts = parts, on_finished = self.sendingFinished) + Logger.log("d", "Syncing material {material_id} with cluster.".format(material_id=material_id)) + self.device.postFormWithParts(target="materials/", parts=parts, on_finished=self.sendingFinished) def sendingFinished(self, reply: QNetworkReply): if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: - Logger.log("e", "Received error code from printer when syncing material: {code}".format(code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute))) - Logger.log("e", reply.readAll().data().decode("utf-8")) \ No newline at end of file + Logger.log("e", "Received error code from printer when syncing material: {code}".format( + code=reply.attribute(QNetworkRequest.HttpStatusCodeAttribute))) + Logger.log("e", reply.readAll().data().decode("utf-8")) + + ## Parse the reply from the printer + # + # Parses the reply to a "/materials" request to the printer + # + # \return a dictionary of ClustMaterial objects by GUID + # \throw json.JSONDecodeError Raised when the reply does not contain a valid json string + # \throw KeyErrror Raised when on of the materials does not include a valid guid + @classmethod + def _parseReply(cls, reply: QNetworkReply) -> Dict[str, ClusterMaterial]: + remote_materials_list = json.loads(reply.readAll().data().decode("utf-8")) + return {material["guid"]: ClusterMaterial(**material) for material in remote_materials_list} + + ## Retrieves a list of local materials + # + # Only the new newest version of the local materials is returned + # + # \return a dictionary of LocalMaterial objects by GUID + @classmethod + def _getLocalMaterials(cls): + result = {} + for material in ContainerRegistry.getInstance().findContainersMetadata(type="material"): + try: + localMaterial = LocalMaterial(**material) + + if localMaterial.GUID not in result or localMaterial.version > result.get(localMaterial.GUID).version: + result[localMaterial.GUID] = localMaterial + except (ValueError): + Logger.log("e", "Material {material_id} has invalid version number {number}.".format( + material_id=material["id"], number=material["version"])) + + return result diff --git a/tests/plugins/UM3NetworkPrinting/TestSendMaterialJob.py b/tests/plugins/UM3NetworkPrinting/TestSendMaterialJob.py new file mode 100644 index 0000000000..3cdb73af22 --- /dev/null +++ b/tests/plugins/UM3NetworkPrinting/TestSendMaterialJob.py @@ -0,0 +1,327 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import io +import json +from typing import Any, List +from unittest import TestCase, mock +from unittest.mock import patch, call + +from PyQt5.QtCore import QByteArray + +from UM.Logger import Logger +from UM.MimeTypeDatabase import MimeType +from UM.Settings.ContainerRegistry import ContainerInterface, ContainerRegistryInterface, \ + DefinitionContainerInterface, ContainerRegistry +from plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice import ClusterUM3OutputDevice +from plugins.UM3NetworkPrinting.src.Models import ClusterMaterial +from plugins.UM3NetworkPrinting.src.SendMaterialJob import SendMaterialJob + +# All log entries written to Log.log by the class-under-test are written to this list. It is cleared before each test +# run and check afterwards +_logentries = [] + + +def new_log(*args): + _logentries.append(args) + + +class TestContainerRegistry(ContainerRegistryInterface): + def __init__(self): + self.containersMetaData = None + + def findContainers(self, *, ignore_case: bool = False, **kwargs: Any) -> List[ContainerInterface]: + raise NotImplementedError() + + def findDefinitionContainers(self, **kwargs: Any) -> List[DefinitionContainerInterface]: + raise NotImplementedError() + + @classmethod + def getApplication(cls) -> "Application": + raise NotImplementedError() + + def getEmptyInstanceContainer(self) -> "InstanceContainer": + raise NotImplementedError() + + def isReadOnly(self, container_id: str) -> bool: + raise NotImplementedError() + + def setContainersMetadata(self, value): + self.containersMetaData = value + + def findContainersMetadata(self, type): + return self.containersMetaData + + +class FakeDevice(ClusterUM3OutputDevice): + def _createFormPart(self, content_header, data, content_type=None): + return "xxx" + + +class TestSendMaterialJob(TestCase): + _LOCALMATERIAL_WHITE = {'type': 'material', 'status': 'unknown', 'id': 'generic_pla_white', + 'base_file': 'generic_pla_white', 'setting_version': 5, 'name': 'White PLA', + 'brand': 'Generic', 'material': 'PLA', 'color_name': 'White', + 'GUID': 'badb0ee7-87c8-4f3f-9398-938587b67dce', 'version': '1', 'color_code': '#ffffff', + 'description': 'Test PLA White', 'adhesion_info': 'Use glue.', 'approximate_diameter': '3', + 'properties': {'density': '1.00', 'diameter': '2.85', 'weight': '750'}, + 'definition': 'fdmprinter', 'compatible': True} + _LOCALMATERIAL_BLACK = {'type': 'material', 'status': 'unknown', 'id': 'generic_pla_black', + 'base_file': 'generic_pla_black', 'setting_version': 5, 'name': 'Yellow CPE', + 'brand': 'Ultimaker', 'material': 'CPE', 'color_name': 'Black', + 'GUID': '5fbb362a-41f9-4818-bb43-15ea6df34aa4', 'version': '1', 'color_code': '#000000', + 'description': 'Test PLA Black', 'adhesion_info': 'Use glue.', 'approximate_diameter': '3', + 'properties': {'density': '1.01', 'diameter': '2.85', 'weight': '750'}, + 'definition': 'fdmprinter', 'compatible': True} + + _REMOTEMATERIAL_WHITE = { + "guid": "badb0ee7-87c8-4f3f-9398-938587b67dce", + "material": "PLA", + "brand": "Generic", + "version": 1, + "color": "White", + "density": 1.00 + } + _REMOTEMATERIAL_BLACK = { + "guid": "5fbb362a-41f9-4818-bb43-15ea6df34aa4", + "material": "PLA", + "brand": "Generic", + "version": 2, + "color": "Black", + "density": 1.00 + } + + def setUp(self): + # Make sure the we start with clean (log) slate + _logentries.clear() + + def tearDown(self): + # If there are still log entries that were not checked something is wrong or we must add checks for them + self.assertEqual(len(_logentries), 0) + + @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") + def test_run(self, device_mock): + with mock.patch.object(Logger, 'log', new=new_log): + job = SendMaterialJob(device_mock) + job.run() + + device_mock.get.assert_called_with("materials/", on_finished=job.sendMissingMaterials) + self.assertEqual(0, len(_logentries)) + + @patch("PyQt5.QtNetwork.QNetworkReply") + def test_sendMissingMaterials_withFailedRequest(self, reply_mock): + reply_mock.attribute.return_value = 404 + + with mock.patch.object(Logger, 'log', new=new_log): + SendMaterialJob(None).sendMissingMaterials(reply_mock) + + reply_mock.attribute.assert_called_with(0) + self.assertEqual(reply_mock.method_calls, [call.attribute(0)]) + self._assertLogEntries([('e', "Couldn't request current material storage on printer. Not syncing materials.")], + _logentries) + + @patch("PyQt5.QtNetwork.QNetworkReply") + def test_sendMissingMaterials_withBadJsonAnswer(self, reply_mock): + reply_mock.attribute.return_value = 200 + reply_mock.readAll.return_value = QByteArray(b'Six sick hicks nick six slick bricks with picks and sticks.') + + with mock.patch.object(Logger, 'log', new=new_log): + SendMaterialJob(None).sendMissingMaterials(reply_mock) + + reply_mock.attribute.assert_called_with(0) + self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) + self._assertLogEntries( + [('e', "Request material storage on printer: I didn't understand the printer's answer.")], + _logentries) + + @patch("PyQt5.QtNetwork.QNetworkReply") + def test_sendMissingMaterials_withMissingGuid(self, reply_mock): + reply_mock.attribute.return_value = 200 + remoteMaterialWithoutGuid = self._REMOTEMATERIAL_WHITE.copy() + del remoteMaterialWithoutGuid["guid"] + reply_mock.readAll.return_value = QByteArray(json.dumps([remoteMaterialWithoutGuid]).encode("ascii")) + + with mock.patch.object(Logger, 'log', new=new_log): + SendMaterialJob(None).sendMissingMaterials(reply_mock) + + reply_mock.attribute.assert_called_with(0) + self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) + self._assertLogEntries( + [('e', "Request material storage on printer: Printer's answer was missing GUIDs.")], + _logentries) + + @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: []) + @patch("PyQt5.QtNetwork.QNetworkReply") + def test_sendMissingMaterials_WithInvalidVersionInLocalMaterial(self, reply_mock): + reply_mock.attribute.return_value = 200 + reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTEMATERIAL_WHITE]).encode("ascii")) + + containerRegistry = TestContainerRegistry() + localMaterialWhiteWithInvalidVersion = self._LOCALMATERIAL_WHITE.copy() + localMaterialWhiteWithInvalidVersion["version"] = "one" + containerRegistry.setContainersMetadata([localMaterialWhiteWithInvalidVersion]) + + with mock.patch.object(Logger, "log", new=new_log): + with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): + SendMaterialJob(None).sendMissingMaterials(reply_mock) + + reply_mock.attribute.assert_called_with(0) + self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) + self._assertLogEntries([('e', "Material generic_pla_white has invalid version number one.")], _logentries) + + @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: []) + @patch("PyQt5.QtNetwork.QNetworkReply") + def test_sendMissingMaterials_WithMultipleLocalVersionsLowFirst(self, reply_mock): + reply_mock.attribute.return_value = 200 + reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTEMATERIAL_WHITE]).encode("ascii")) + + containerRegistry = TestContainerRegistry() + localMaterialWhiteWithHigherVersion = self._LOCALMATERIAL_WHITE.copy() + localMaterialWhiteWithHigherVersion["version"] = "2" + containerRegistry.setContainersMetadata([self._LOCALMATERIAL_WHITE, localMaterialWhiteWithHigherVersion]) + + with mock.patch.object(Logger, "log", new=new_log): + with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): + SendMaterialJob(None).sendMissingMaterials(reply_mock) + + reply_mock.attribute.assert_called_with(0) + self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) + self._assertLogEntries([], _logentries) + + @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: []) + @patch("PyQt5.QtNetwork.QNetworkReply") + def test_sendMissingMaterials_MaterialMissingOnPrinter(self, reply_mock): + reply_mock.attribute.return_value = 200 + reply_mock.readAll.return_value = QByteArray( + json.dumps([self._REMOTEMATERIAL_WHITE]).encode("ascii")) + + containerRegistry = TestContainerRegistry() + containerRegistry.setContainersMetadata([self._LOCALMATERIAL_WHITE, self._LOCALMATERIAL_BLACK]) + + with mock.patch.object(Logger, "log", new=new_log): + with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): + SendMaterialJob(None).sendMissingMaterials(reply_mock) + + reply_mock.attribute.assert_called_with(0) + self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) + self._assertLogEntries([], _logentries) + + @patch("builtins.open", lambda a, b: io.StringIO("")) + @patch("UM.MimeTypeDatabase.MimeTypeDatabase.getMimeTypeForFile", + lambda _: MimeType(name="application/x-ultimaker-material-profile", comment="Ultimaker Material Profile", + suffixes=["xml.fdm_material"])) + @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: ["/materials/generic_pla_white.xml.fdm_material"]) + @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") + def test_sendMaterialsToPrinter(self, device): + with mock.patch.object(Logger, "log", new=new_log): + SendMaterialJob(device).sendMaterialsToPrinter({'generic_pla_white'}) + + self._assertLogEntries([("d", "Syncing material generic_pla_white with cluster.")], _logentries) + + @patch("PyQt5.QtNetwork.QNetworkReply") + def xtest_sendMissingMaterials(self, reply_mock): + reply_mock.attribute.return_value = 200 + reply_mock.readAll.return_value = QByteArray( + json.dumps([self._REMOTEMATERIAL_WHITE], self._REMOTEMATERIAL_BLACK).encode("ascii")) + + containerRegistry = TestContainerRegistry() + containerRegistry.setContainersMetadata([self._LOCALMATERIAL_WHITE, self._LOCALMATERIAL_BLACK]) + + with mock.patch.object(Logger, "log", new=new_log): + with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): + SendMaterialJob(None).sendMissingMaterials(reply_mock) + + reply_mock.attribute.assert_called_with(0) + self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) + self._assertLogEntries([], _logentries) + + @patch("PyQt5.QtNetwork.QNetworkReply") + def test_sendingFinished_success(self, reply_mock) -> None: + reply_mock.attribute.return_value = 200 + with mock.patch.object(Logger, 'log', new=new_log): + SendMaterialJob(None).sendingFinished(reply_mock) + + reply_mock.attribute.assert_called_once_with(0) + self.assertEqual(0, len(_logentries)) + + @patch("PyQt5.QtNetwork.QNetworkReply") + def test_sendingFinished_failed(self, reply_mock) -> None: + reply_mock.attribute.return_value = 404 + reply_mock.readAll.return_value = QByteArray(b'Six sick hicks nick six slick bricks with picks and sticks.') + + with mock.patch.object(Logger, 'log', new=new_log): + SendMaterialJob(None).sendingFinished(reply_mock) + + reply_mock.attribute.assert_called_with(0) + self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.attribute(0), call.readAll()]) + + self._assertLogEntries([ + ("e", "Received error code from printer when syncing material: 404"), + ("e", "Six sick hicks nick six slick bricks with picks and sticks.") + ], _logentries) + + @patch("PyQt5.QtNetwork.QNetworkReply") + def test_parseReply(self, reply_mock): + reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTEMATERIAL_WHITE]).encode("ascii")) + + response = SendMaterialJob._parseReply(reply_mock) + + self.assertTrue(len(response) == 1) + self.assertEqual(next(iter(response.values())), ClusterMaterial(**self._REMOTEMATERIAL_WHITE)) + + @patch("PyQt5.QtNetwork.QNetworkReply") + def test_parseReplyWithInvalidMaterial(self, reply_mock): + remoteMaterialWithInvalidVersion = self._REMOTEMATERIAL_WHITE.copy() + remoteMaterialWithInvalidVersion["version"] = "one" + reply_mock.readAll.return_value = QByteArray(json.dumps([remoteMaterialWithInvalidVersion]).encode("ascii")) + + with self.assertRaises(ValueError): + SendMaterialJob._parseReply(reply_mock) + + def test__getLocalMaterials(self): + containerRegistry = TestContainerRegistry() + containerRegistry.setContainersMetadata([self._LOCALMATERIAL_WHITE, self._LOCALMATERIAL_BLACK]) + + with mock.patch.object(Logger, "log", new=new_log): + with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): + local_materials = SendMaterialJob(None)._getLocalMaterials() + + self.assertTrue(len(local_materials) == 2) + + def test__getLocalMaterialsWithMultipleVersions(self): + containerRegistry = TestContainerRegistry() + localMaterialWithNewerVersion = self._LOCALMATERIAL_WHITE.copy() + localMaterialWithNewerVersion["version"] = 2 + containerRegistry.setContainersMetadata([self._LOCALMATERIAL_WHITE, localMaterialWithNewerVersion]) + + with mock.patch.object(Logger, "log", new=new_log): + with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): + local_materials = SendMaterialJob(None)._getLocalMaterials() + + self.assertTrue(len(local_materials) == 1) + self.assertTrue(list(local_materials.values())[0].version == 2) + + containerRegistry.setContainersMetadata([localMaterialWithNewerVersion, self._LOCALMATERIAL_WHITE]) + + with mock.patch.object(Logger, "log", new=new_log): + with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): + local_materials = SendMaterialJob(None)._getLocalMaterials() + + self.assertTrue(len(local_materials) == 1) + self.assertTrue(list(local_materials.values())[0].version == 2) + + def _assertLogEntries(self, first, second): + """ + Inspects the two sets of log entry tuples and fails when they are not the same + :param first: The first set of tuples + :param second: The second set of tuples + """ + self.assertEqual(len(first), len(second)) + + while len(first) > 0: + e1, m1 = first[0] + e2, m2 = second[0] + self.assertEqual(e1, e2) + self.assertEqual(m1, m2) + first.pop(0) + second.pop(0) From 421af26f87892484381d398a6cbdf05e96a26165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijn=20De=C3=A9?= Date: Fri, 16 Nov 2018 14:54:49 +0100 Subject: [PATCH 022/146] Extra test on test_sendMaterialsToPrinter, removed unused code --- .../UM3NetworkPrinting/TestSendMaterialJob.py | 25 +++++-------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/tests/plugins/UM3NetworkPrinting/TestSendMaterialJob.py b/tests/plugins/UM3NetworkPrinting/TestSendMaterialJob.py index 3cdb73af22..172b42cb8b 100644 --- a/tests/plugins/UM3NetworkPrinting/TestSendMaterialJob.py +++ b/tests/plugins/UM3NetworkPrinting/TestSendMaterialJob.py @@ -212,28 +212,15 @@ class TestSendMaterialJob(TestCase): suffixes=["xml.fdm_material"])) @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: ["/materials/generic_pla_white.xml.fdm_material"]) @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") - def test_sendMaterialsToPrinter(self, device): + def test_sendMaterialsToPrinter(self, device_mock): + device_mock._createFormPart.return_value = "_xXx_" with mock.patch.object(Logger, "log", new=new_log): - SendMaterialJob(device).sendMaterialsToPrinter({'generic_pla_white'}) + job = SendMaterialJob(device_mock) + job.sendMaterialsToPrinter({'generic_pla_white'}) self._assertLogEntries([("d", "Syncing material generic_pla_white with cluster.")], _logentries) - - @patch("PyQt5.QtNetwork.QNetworkReply") - def xtest_sendMissingMaterials(self, reply_mock): - reply_mock.attribute.return_value = 200 - reply_mock.readAll.return_value = QByteArray( - json.dumps([self._REMOTEMATERIAL_WHITE], self._REMOTEMATERIAL_BLACK).encode("ascii")) - - containerRegistry = TestContainerRegistry() - containerRegistry.setContainersMetadata([self._LOCALMATERIAL_WHITE, self._LOCALMATERIAL_BLACK]) - - with mock.patch.object(Logger, "log", new=new_log): - with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): - SendMaterialJob(None).sendMissingMaterials(reply_mock) - - reply_mock.attribute.assert_called_with(0) - self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) - self._assertLogEntries([], _logentries) + self.assertEqual([call._createFormPart('name="file"; filename="generic_pla_white.xml.fdm_material"', ''), + call.postFormWithParts(on_finished=job.sendingFinished, parts = ["_xXx_"], target = "materials/")], device_mock.method_calls) @patch("PyQt5.QtNetwork.QNetworkReply") def test_sendingFinished_success(self, reply_mock) -> None: From 695d45ffbed22396035f0d32f616e610712a1923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijn=20De=C3=A9?= Date: Fri, 16 Nov 2018 15:54:07 +0100 Subject: [PATCH 023/146] Initialize the models with None --- plugins/UM3NetworkPrinting/src/Models.py | 48 ++++++++++++------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Models.py b/plugins/UM3NetworkPrinting/src/Models.py index 6d42b39370..89bf665377 100644 --- a/plugins/UM3NetworkPrinting/src/Models.py +++ b/plugins/UM3NetworkPrinting/src/Models.py @@ -18,17 +18,17 @@ class ClusterMaterial(BaseModel): self.version = int(self.version) self.density = float(self.density) - guid: None # type: Optional[str] + guid = None # type: Optional[str] - material: None # type: Optional[str] + material = None # type: Optional[str] - brand: None # type: Optional[str] + brand = None # type: Optional[str] version = None # type: Optional[int] - color: None # type: Optional[str] + color = None # type: Optional[str] - density: None # type: Optional[float] + density = None # type: Optional[float] class LocalMaterialProperties(BaseModel): @@ -38,11 +38,11 @@ class LocalMaterialProperties(BaseModel): self.diameter = float(self.diameter) self.weight = float(self.weight) - density: None # type: Optional[float] + density = None # type: Optional[float] - diameter: None # type: Optional[float] + diameter = None # type: Optional[float] - weight: None # type: Optional[int] + weight = None # type: Optional[int] class LocalMaterial(BaseModel): @@ -52,36 +52,36 @@ class LocalMaterial(BaseModel): self.approximate_diameter = float(self.approximate_diameter) self.version = int(self.version) - GUID: None # type: Optional[str] + GUID = None # type: Optional[str] - id: None # type: Optional[str] + id = None # type: Optional[str] - type: None # type: Optional[str] + type = None # type: Optional[str] - status: None # type: Optional[str] + status = None # type: Optional[str] - base_file: None # type: Optional[str] + base_file = None # type: Optional[str] - setting_version: None # type: Optional[str] + setting_version = None # type: Optional[str] version = None # type: Optional[int] - name: None # type: Optional[str] + name = None # type: Optional[str] - brand: None # type: Optional[str] + brand = None # type: Optional[str] - material: None # type: Optional[str] + material = None # type: Optional[str] - color_name: None # type: Optional[str] + color_name = None # type: Optional[str] - description: None # type: Optional[str] + description = None # type: Optional[str] - adhesion_info: None # type: Optional[str] + adhesion_info = None # type: Optional[str] - approximate_diameter: None # type: Optional[float] + approximate_diameter = None # type: Optional[float] - properties: None # type: LocalMaterialProperties + properties = None # type: LocalMaterialProperties - definition: None # type: Optional[str] + definition = None # type: Optional[str] - compatible: None # type: Optional[bool] + compatible = None # type: Optional[bool] From 0062250238030932b15dd36511c96c4bc1e6af3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijn=20De=C3=A9?= Date: Fri, 16 Nov 2018 16:06:36 +0100 Subject: [PATCH 024/146] Moved the test to the plugin directory --- .../UM3NetworkPrinting/tests}/TestSendMaterialJob.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {tests/plugins/UM3NetworkPrinting => plugins/UM3NetworkPrinting/tests}/TestSendMaterialJob.py (100%) diff --git a/tests/plugins/UM3NetworkPrinting/TestSendMaterialJob.py b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py similarity index 100% rename from tests/plugins/UM3NetworkPrinting/TestSendMaterialJob.py rename to plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py From 1000625d6947fe701a25cdff5f13c2ff045e29f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijn=20De=C3=A9?= Date: Fri, 16 Nov 2018 16:07:17 +0100 Subject: [PATCH 025/146] Added spaces around all = --- .../UM3NetworkPrinting/src/SendMaterialJob.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py index 126ed07317..baea3b9a78 100644 --- a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py @@ -30,7 +30,7 @@ class SendMaterialJob(Job): self.device = device # type: ClusterUM3OutputDevice def run(self) -> None: - self.device.get("materials/", on_finished=self.sendMissingMaterials) + self.device.get("materials/", on_finished = self.sendMissingMaterials) def sendMissingMaterials(self, reply: QNetworkReply) -> None: if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: # Got an error from the HTTP request. @@ -77,23 +77,23 @@ class SendMaterialJob(Job): parts = [] with open(file_path, "rb") as f: parts.append( - self.device._createFormPart("name=\"file\"; filename=\"{file_name}\"".format(file_name=file_name), + self.device._createFormPart("name=\"file\"; filename=\"{file_name}\"".format(file_name = file_name), f.read())) signature_file_path = file_path + ".sig" if os.path.exists(signature_file_path): _, signature_file_name = os.path.split(signature_file_path) with open(signature_file_path, "rb") as f: parts.append(self.device._createFormPart( - "name=\"signature_file\"; filename=\"{file_name}\"".format(file_name=signature_file_name), + "name=\"signature_file\"; filename=\"{file_name}\"".format(file_name = signature_file_name), f.read())) - Logger.log("d", "Syncing material {material_id} with cluster.".format(material_id=material_id)) - self.device.postFormWithParts(target="materials/", parts=parts, on_finished=self.sendingFinished) + Logger.log("d", "Syncing material {material_id} with cluster.".format(material_id = material_id)) + self.device.postFormWithParts(target = "materials/", parts = parts, on_finished = self.sendingFinished) def sendingFinished(self, reply: QNetworkReply): if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: Logger.log("e", "Received error code from printer when syncing material: {code}".format( - code=reply.attribute(QNetworkRequest.HttpStatusCodeAttribute))) + code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute))) Logger.log("e", reply.readAll().data().decode("utf-8")) ## Parse the reply from the printer @@ -116,7 +116,7 @@ class SendMaterialJob(Job): @classmethod def _getLocalMaterials(cls): result = {} - for material in ContainerRegistry.getInstance().findContainersMetadata(type="material"): + for material in ContainerRegistry.getInstance().findContainersMetadata(type = "material"): try: localMaterial = LocalMaterial(**material) @@ -124,6 +124,6 @@ class SendMaterialJob(Job): result[localMaterial.GUID] = localMaterial except (ValueError): Logger.log("e", "Material {material_id} has invalid version number {number}.".format( - material_id=material["id"], number=material["version"])) + material_id = material["id"], number = material["version"])) return result From 565f009e9b6ca4b447145b38c2aa6cb3ef8d4169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijn=20De=C3=A9?= Date: Fri, 16 Nov 2018 16:34:44 +0100 Subject: [PATCH 026/146] Added comments --- .../UM3NetworkPrinting/src/SendMaterialJob.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py index baea3b9a78..62b98bcdbd 100644 --- a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py @@ -1,7 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -import json # To understand the list of materials from the printer reply. +import json # To decode the list of materials from the printer reply. import os # To walk over material files. import os.path # To filter on material files. import urllib.parse # For getting material IDs from their file names. @@ -29,9 +29,13 @@ class SendMaterialJob(Job): super().__init__() self.device = device # type: ClusterUM3OutputDevice + ## Send the request to the printer and register a callback def run(self) -> None: self.device.get("materials/", on_finished = self.sendMissingMaterials) + ## Process the reply from the printer and determine which materials should be updated and sent to the printer + # + # \param reply The reply from the printer, a json file def sendMissingMaterials(self, reply: QNetworkReply) -> None: if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: # Got an error from the HTTP request. Logger.log("e", "Couldn't request current material storage on printer. Not syncing materials.") @@ -50,7 +54,7 @@ class SendMaterialJob(Job): # Collect local materials local_materials_by_guid = self._getLocalMaterials() - # Find out what materials are new or updated annd must be sent to the printer + # Find out what materials are new or updated and must be sent to the printer materials_to_send = { material.id for guid, material in local_materials_by_guid.items() @@ -61,7 +65,13 @@ class SendMaterialJob(Job): # Send materials to the printer self.sendMaterialsToPrinter(materials_to_send) - def sendMaterialsToPrinter(self, materials_to_send): + ## Send the materials to the printer + # + # The given materials will be loaded from disk en sent to to printer. The given id's will be mathed with + # filenames of the locally stored materials + # + # \param materials_to_send A set with id's of materials that must be sent + def sendMaterialsToPrinter(self, materials_to_send) -> None: for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.MaterialInstanceContainer): try: mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path) @@ -90,7 +100,8 @@ class SendMaterialJob(Job): Logger.log("d", "Syncing material {material_id} with cluster.".format(material_id = material_id)) self.device.postFormWithParts(target = "materials/", parts = parts, on_finished = self.sendingFinished) - def sendingFinished(self, reply: QNetworkReply): + ## Check a reply from an upload to the printer and log an error when the call failed + def sendingFinished(self, reply: QNetworkReply) -> None: if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: Logger.log("e", "Received error code from printer when syncing material: {code}".format( code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute))) From 22aac7b5665e6dbcda2107be8e0831b11e8d28b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijn=20De=C3=A9?= Date: Fri, 16 Nov 2018 16:35:08 +0100 Subject: [PATCH 027/146] Renamed TestContainerRegistry to ContainerRegistryMock --- .../UM3NetworkPrinting/tests/TestSendMaterialJob.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py index 172b42cb8b..03d2a81e89 100644 --- a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py @@ -26,7 +26,7 @@ def new_log(*args): _logentries.append(args) -class TestContainerRegistry(ContainerRegistryInterface): +class ContainerRegistryMock(ContainerRegistryInterface): def __init__(self): self.containersMetaData = None @@ -156,7 +156,7 @@ class TestSendMaterialJob(TestCase): reply_mock.attribute.return_value = 200 reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTEMATERIAL_WHITE]).encode("ascii")) - containerRegistry = TestContainerRegistry() + containerRegistry = ContainerRegistryMock() localMaterialWhiteWithInvalidVersion = self._LOCALMATERIAL_WHITE.copy() localMaterialWhiteWithInvalidVersion["version"] = "one" containerRegistry.setContainersMetadata([localMaterialWhiteWithInvalidVersion]) @@ -175,7 +175,7 @@ class TestSendMaterialJob(TestCase): reply_mock.attribute.return_value = 200 reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTEMATERIAL_WHITE]).encode("ascii")) - containerRegistry = TestContainerRegistry() + containerRegistry = ContainerRegistryMock() localMaterialWhiteWithHigherVersion = self._LOCALMATERIAL_WHITE.copy() localMaterialWhiteWithHigherVersion["version"] = "2" containerRegistry.setContainersMetadata([self._LOCALMATERIAL_WHITE, localMaterialWhiteWithHigherVersion]) @@ -195,7 +195,7 @@ class TestSendMaterialJob(TestCase): reply_mock.readAll.return_value = QByteArray( json.dumps([self._REMOTEMATERIAL_WHITE]).encode("ascii")) - containerRegistry = TestContainerRegistry() + containerRegistry = ContainerRegistryMock() containerRegistry.setContainersMetadata([self._LOCALMATERIAL_WHITE, self._LOCALMATERIAL_BLACK]) with mock.patch.object(Logger, "log", new=new_log): @@ -266,7 +266,7 @@ class TestSendMaterialJob(TestCase): SendMaterialJob._parseReply(reply_mock) def test__getLocalMaterials(self): - containerRegistry = TestContainerRegistry() + containerRegistry = ContainerRegistryMock() containerRegistry.setContainersMetadata([self._LOCALMATERIAL_WHITE, self._LOCALMATERIAL_BLACK]) with mock.patch.object(Logger, "log", new=new_log): @@ -276,7 +276,7 @@ class TestSendMaterialJob(TestCase): self.assertTrue(len(local_materials) == 2) def test__getLocalMaterialsWithMultipleVersions(self): - containerRegistry = TestContainerRegistry() + containerRegistry = ContainerRegistryMock() localMaterialWithNewerVersion = self._LOCALMATERIAL_WHITE.copy() localMaterialWithNewerVersion["version"] = 2 containerRegistry.setContainersMetadata([self._LOCALMATERIAL_WHITE, localMaterialWithNewerVersion]) From 23e957e1c5c8fdbb36368f319feb57218dd7ab92 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 19 Nov 2018 10:44:24 +0100 Subject: [PATCH 028/146] Some more refactoring, splitting up methods --- .../NetworkedPrinterOutputDevice.py | 3 + .../UM3NetworkPrinting/src/SendMaterialJob.py | 180 +++++++++++------- 2 files changed, 115 insertions(+), 68 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 35d2ce014a..9a3be936a2 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -147,6 +147,9 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent) return request + def createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart: + return self._createFormPart(content_header, data, content_type) + def _createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart: part = QHttpPart() diff --git a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py index 62b98bcdbd..6763901151 100644 --- a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py @@ -1,21 +1,20 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import json +import os +import urllib.parse +from typing import Dict, TYPE_CHECKING, Set -import json # To decode the list of materials from the printer reply. -import os # To walk over material files. -import os.path # To filter on material files. -import urllib.parse # For getting material IDs from their file names. -from typing import Dict, TYPE_CHECKING +from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest -from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest # To listen to the reply from the printer. - -from UM.Job import Job # The interface we're implementing. +from UM.Job import Job from UM.Logger import Logger -from UM.MimeTypeDatabase import MimeTypeDatabase # To strip the extensions of the material profile files. +from UM.MimeTypeDatabase import MimeTypeDatabase from UM.Resources import Resources -from UM.Settings.ContainerRegistry import ContainerRegistry # To find the GUIDs of materials. -from cura.CuraApplication import CuraApplication # For the resource types. -from plugins.UM3NetworkPrinting.src.Models import ClusterMaterial, LocalMaterial +from cura.CuraApplication import CuraApplication + +# Absolute imports don't work in plugins +from .Models import ClusterMaterial, LocalMaterial if TYPE_CHECKING: from .ClusterUM3OutputDevice import ClusterUM3OutputDevice @@ -25,95 +24,136 @@ if TYPE_CHECKING: # # This way it won't freeze up the interface while sending those materials. class SendMaterialJob(Job): + def __init__(self, device: "ClusterUM3OutputDevice") -> None: super().__init__() self.device = device # type: ClusterUM3OutputDevice + self._application = CuraApplication.getInstance() # type: CuraApplication ## Send the request to the printer and register a callback def run(self) -> None: self.device.get("materials/", on_finished = self.sendMissingMaterials) - ## Process the reply from the printer and determine which materials should be updated and sent to the printer + ## Process the materials reply from the printer. # - # \param reply The reply from the printer, a json file - def sendMissingMaterials(self, reply: QNetworkReply) -> None: - if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: # Got an error from the HTTP request. - Logger.log("e", "Couldn't request current material storage on printer. Not syncing materials.") + # \param reply The reply from the printer, a json file. + def _onGetRemoteMaterials(self, reply: QNetworkReply) -> None: + + # Got an error from the HTTP request. If we did not receive a 200 something happened. + if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: + Logger.log("e", "Error fetching materials from printer: %s", reply.errorString()) return - # Collect materials from the printer's reply + # Collect materials from the printer's reply and send the missing ones if needed. try: remote_materials_by_guid = self._parseReply(reply) - except json.JSONDecodeError: - Logger.log("e", "Request material storage on printer: I didn't understand the printer's answer.") - return - except KeyError: - Logger.log("e", "Request material storage on printer: Printer's answer was missing GUIDs.") - return + self._sendMissingMaterials(remote_materials_by_guid) + except json.JSONDecodeError as e: + Logger.log("e", "Error parsing materials from printer: %s", e) + except KeyError as e: + Logger.log("e", "Error parsing materials from printer: %s", e) + + ## Determine which materials should be updated and send them to the printer. + # + # \param remote_materials_by_guid The remote materials by GUID. + def _sendMissingMaterials(self, remote_materials_by_guid: Dict[str, ClusterMaterial]) -> None: # Collect local materials local_materials_by_guid = self._getLocalMaterials() + if len(local_materials_by_guid) == 0: + Logger.log("d", "There are no local materials to synchronize with the printer.") + return # Find out what materials are new or updated and must be sent to the printer - materials_to_send = { - material.id - for guid, material in local_materials_by_guid.items() - if guid not in remote_materials_by_guid or - material.version > remote_materials_by_guid[guid].version - } + material_ids_to_send = self._determineMaterialsToSend(local_materials_by_guid, remote_materials_by_guid) + if len(material_ids_to_send) == 0: + Logger.log("d", "There are no remote materials to update.") + return # Send materials to the printer - self.sendMaterialsToPrinter(materials_to_send) + self._sendMaterials(material_ids_to_send) - ## Send the materials to the printer + ## From the local and remote materials, determine which ones should be synchronized. # - # The given materials will be loaded from disk en sent to to printer. The given id's will be mathed with - # filenames of the locally stored materials + # Makes a Set containing only the materials that are not on the printer yet or the ones that are newer in Cura. # - # \param materials_to_send A set with id's of materials that must be sent - def sendMaterialsToPrinter(self, materials_to_send) -> None: - for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.MaterialInstanceContainer): + # \param local_materials The local materials by GUID. + # \param remote_materials The remote materials by GUID. + @staticmethod + def _determineMaterialsToSend(local_materials: Dict[str, LocalMaterial], + remote_materials: Dict[str, ClusterMaterial]) -> Set[str]: + return { + material.id for guid, material in local_materials.items() + if guid not in remote_materials or material.version > remote_materials[guid].version + } + + ## Send the materials to the printer. + # + # The given materials will be loaded from disk en sent to to printer. + # The given id's will be matched with filenames of the locally stored materials. + # + # \param materials_to_send A set with id's of materials that must be sent. + def _sendMaterials(self, materials_to_send: Set[str]) -> None: + file_paths = Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.MaterialInstanceContainer) + + # Find all local material files and send them if needed. + for file_path in file_paths: try: mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path) except MimeTypeDatabase.MimeTypeNotFoundError: - continue # Not the sort of file we'd like to send then. - - _, file_name = os.path.split(file_path) - material_id = urllib.parse.unquote_plus(mime_type.stripExtension(file_name)) - - if material_id not in materials_to_send: continue + file_name = os.path.basename(file_path) + material_id = urllib.parse.unquote_plus(mime_type.stripExtension(file_name)) + if material_id not in materials_to_send: + # If the material does not have to be sent we skip it. + continue + + self._sendMaterialFile(file_path, file_name, material_id) + + ## Send a single material file to the printer. + # + # Also add the material signature file if that is available. + # + # \param file_path The path of the material file. + # \param file_name The name of the material file. + # \param material_id The ID of the material in the file. + def _sendMaterialFile(self, file_path: str, file_name: str, material_id: str) -> None: + parts = [] + + # Add the material file. with open(file_path, "rb") as f: - parts.append( - self.device._createFormPart("name=\"file\"; filename=\"{file_name}\"".format(file_name = file_name), - f.read())) - signature_file_path = file_path + ".sig" + parts.append(self.device.createFormPart("name=\"file\"; filename=\"{file_name}\"" + .format(file_name = file_name), f.read())) + + # Add the material signature file if needed. + signature_file_path = "{}.sig".format(file_path) if os.path.exists(signature_file_path): - _, signature_file_name = os.path.split(signature_file_path) + signature_file_name = os.path.basename(signature_file_path) with open(signature_file_path, "rb") as f: - parts.append(self.device._createFormPart( - "name=\"signature_file\"; filename=\"{file_name}\"".format(file_name = signature_file_name), - f.read())) + parts.append(self.device.createFormPart("name=\"signature_file\"; filename=\"{file_name}\"" + .format(file_name = signature_file_name), f.read())) Logger.log("d", "Syncing material {material_id} with cluster.".format(material_id = material_id)) self.device.postFormWithParts(target = "materials/", parts = parts, on_finished = self.sendingFinished) ## Check a reply from an upload to the printer and log an error when the call failed - def sendingFinished(self, reply: QNetworkReply) -> None: + @staticmethod + def sendingFinished(reply: QNetworkReply) -> None: if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: - Logger.log("e", "Received error code from printer when syncing material: {code}".format( - code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute))) - Logger.log("e", reply.readAll().data().decode("utf-8")) + Logger.log("e", "Received error code from printer when syncing material: {code}, {text}".format( + code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), + text = reply.errorString() + )) ## Parse the reply from the printer # # Parses the reply to a "/materials" request to the printer # - # \return a dictionary of ClustMaterial objects by GUID + # \return a dictionary of ClusterMaterial objects by GUID # \throw json.JSONDecodeError Raised when the reply does not contain a valid json string - # \throw KeyErrror Raised when on of the materials does not include a valid guid + # \throw KeyError Raised when on of the materials does not include a valid guid @classmethod def _parseReply(cls, reply: QNetworkReply) -> Dict[str, ClusterMaterial]: remote_materials_list = json.loads(reply.readAll().data().decode("utf-8")) @@ -124,17 +164,21 @@ class SendMaterialJob(Job): # Only the new newest version of the local materials is returned # # \return a dictionary of LocalMaterial objects by GUID - @classmethod - def _getLocalMaterials(cls): - result = {} - for material in ContainerRegistry.getInstance().findContainersMetadata(type = "material"): - try: - localMaterial = LocalMaterial(**material) + def _getLocalMaterials(self) -> Dict[str, LocalMaterial]: + result = {} # type: Dict[str, LocalMaterial] + container_registry = self._application.getContainerRegistry() + material_containers = container_registry.findContainersMetadata(type = "material") - if localMaterial.GUID not in result or localMaterial.version > result.get(localMaterial.GUID).version: - result[localMaterial.GUID] = localMaterial - except (ValueError): - Logger.log("e", "Material {material_id} has invalid version number {number}.".format( - material_id = material["id"], number = material["version"])) + # Find the latest version of all material containers in the registry. + for m in material_containers: + try: + material = LocalMaterial(**m) + if material.GUID not in result or material.version > result.get(material.GUID).version: + result[material.GUID] = material + except ValueError as e: + Logger.log("w", "Local material {material_id} has invalid values: {e}".format( + material_id = m["id"], + e = e + )) return result From ee9210d8d1ce895d22636c74d8bf3218f39460f2 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 19 Nov 2018 10:57:47 +0100 Subject: [PATCH 029/146] Rewrite tests --- .../UM3NetworkPrinting/src/SendMaterialJob.py | 2 +- .../tests/TestSendMaterialJob.py | 417 +++++++++--------- 2 files changed, 198 insertions(+), 221 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py index 6763901151..349e6929ff 100644 --- a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py @@ -32,7 +32,7 @@ class SendMaterialJob(Job): ## Send the request to the printer and register a callback def run(self) -> None: - self.device.get("materials/", on_finished = self.sendMissingMaterials) + self.device.get("materials/", on_finished = self._onGetRemoteMaterials) ## Process the materials reply from the printer. # diff --git a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py index 03d2a81e89..bc6e1def14 100644 --- a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py @@ -1,6 +1,5 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. - import io import json from typing import Any, List @@ -17,16 +16,9 @@ from plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice import ClusterUM3Outp from plugins.UM3NetworkPrinting.src.Models import ClusterMaterial from plugins.UM3NetworkPrinting.src.SendMaterialJob import SendMaterialJob -# All log entries written to Log.log by the class-under-test are written to this list. It is cleared before each test -# run and check afterwards -_logentries = [] - - -def new_log(*args): - _logentries.append(args) - class ContainerRegistryMock(ContainerRegistryInterface): + def __init__(self): self.containersMetaData = None @@ -59,14 +51,16 @@ class FakeDevice(ClusterUM3OutputDevice): class TestSendMaterialJob(TestCase): - _LOCALMATERIAL_WHITE = {'type': 'material', 'status': 'unknown', 'id': 'generic_pla_white', + + _LOCAL_MATERIAL_WHITE = {'type': 'material', 'status': 'unknown', 'id': 'generic_pla_white', 'base_file': 'generic_pla_white', 'setting_version': 5, 'name': 'White PLA', 'brand': 'Generic', 'material': 'PLA', 'color_name': 'White', 'GUID': 'badb0ee7-87c8-4f3f-9398-938587b67dce', 'version': '1', 'color_code': '#ffffff', 'description': 'Test PLA White', 'adhesion_info': 'Use glue.', 'approximate_diameter': '3', 'properties': {'density': '1.00', 'diameter': '2.85', 'weight': '750'}, 'definition': 'fdmprinter', 'compatible': True} - _LOCALMATERIAL_BLACK = {'type': 'material', 'status': 'unknown', 'id': 'generic_pla_black', + + _LOCAL_MATERIAL_BLACK = {'type': 'material', 'status': 'unknown', 'id': 'generic_pla_black', 'base_file': 'generic_pla_black', 'setting_version': 5, 'name': 'Yellow CPE', 'brand': 'Ultimaker', 'material': 'CPE', 'color_name': 'Black', 'GUID': '5fbb362a-41f9-4818-bb43-15ea6df34aa4', 'version': '1', 'color_code': '#000000', @@ -74,7 +68,7 @@ class TestSendMaterialJob(TestCase): 'properties': {'density': '1.01', 'diameter': '2.85', 'weight': '750'}, 'definition': 'fdmprinter', 'compatible': True} - _REMOTEMATERIAL_WHITE = { + _REMOTE_MATERIAL_WHITE = { "guid": "badb0ee7-87c8-4f3f-9398-938587b67dce", "material": "PLA", "brand": "Generic", @@ -82,7 +76,8 @@ class TestSendMaterialJob(TestCase): "color": "White", "density": 1.00 } - _REMOTEMATERIAL_BLACK = { + + _REMOTE_MATERIAL_BLACK = { "guid": "5fbb362a-41f9-4818-bb43-15ea6df34aa4", "material": "PLA", "brand": "Generic", @@ -91,224 +86,206 @@ class TestSendMaterialJob(TestCase): "density": 1.00 } - def setUp(self): - # Make sure the we start with clean (log) slate - _logentries.clear() - - def tearDown(self): - # If there are still log entries that were not checked something is wrong or we must add checks for them - self.assertEqual(len(_logentries), 0) - @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") def test_run(self, device_mock): - with mock.patch.object(Logger, 'log', new=new_log): - job = SendMaterialJob(device_mock) - job.run() - - device_mock.get.assert_called_with("materials/", on_finished=job.sendMissingMaterials) - self.assertEqual(0, len(_logentries)) + job = SendMaterialJob(device_mock) + job.run() + device_mock.get.assert_called_with("materials/", on_finished=job._onGetRemoteMaterials) + @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") @patch("PyQt5.QtNetwork.QNetworkReply") - def test_sendMissingMaterials_withFailedRequest(self, reply_mock): + def test_sendMissingMaterials_withFailedRequest(self, reply_mock, device_mock): reply_mock.attribute.return_value = 404 - - with mock.patch.object(Logger, 'log', new=new_log): - SendMaterialJob(None).sendMissingMaterials(reply_mock) - + SendMaterialJob(device_mock).run() reply_mock.attribute.assert_called_with(0) self.assertEqual(reply_mock.method_calls, [call.attribute(0)]) - self._assertLogEntries([('e', "Couldn't request current material storage on printer. Not syncing materials.")], - _logentries) + self.assertEqual(device_mock._onGetRemoteMaterials.method_calls, []) - @patch("PyQt5.QtNetwork.QNetworkReply") - def test_sendMissingMaterials_withBadJsonAnswer(self, reply_mock): - reply_mock.attribute.return_value = 200 - reply_mock.readAll.return_value = QByteArray(b'Six sick hicks nick six slick bricks with picks and sticks.') - - with mock.patch.object(Logger, 'log', new=new_log): - SendMaterialJob(None).sendMissingMaterials(reply_mock) - - reply_mock.attribute.assert_called_with(0) - self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) - self._assertLogEntries( - [('e', "Request material storage on printer: I didn't understand the printer's answer.")], - _logentries) - - @patch("PyQt5.QtNetwork.QNetworkReply") - def test_sendMissingMaterials_withMissingGuid(self, reply_mock): - reply_mock.attribute.return_value = 200 - remoteMaterialWithoutGuid = self._REMOTEMATERIAL_WHITE.copy() - del remoteMaterialWithoutGuid["guid"] - reply_mock.readAll.return_value = QByteArray(json.dumps([remoteMaterialWithoutGuid]).encode("ascii")) - - with mock.patch.object(Logger, 'log', new=new_log): - SendMaterialJob(None).sendMissingMaterials(reply_mock) - - reply_mock.attribute.assert_called_with(0) - self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) - self._assertLogEntries( - [('e', "Request material storage on printer: Printer's answer was missing GUIDs.")], - _logentries) - - @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: []) - @patch("PyQt5.QtNetwork.QNetworkReply") - def test_sendMissingMaterials_WithInvalidVersionInLocalMaterial(self, reply_mock): - reply_mock.attribute.return_value = 200 - reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTEMATERIAL_WHITE]).encode("ascii")) - - containerRegistry = ContainerRegistryMock() - localMaterialWhiteWithInvalidVersion = self._LOCALMATERIAL_WHITE.copy() - localMaterialWhiteWithInvalidVersion["version"] = "one" - containerRegistry.setContainersMetadata([localMaterialWhiteWithInvalidVersion]) - - with mock.patch.object(Logger, "log", new=new_log): - with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): - SendMaterialJob(None).sendMissingMaterials(reply_mock) - - reply_mock.attribute.assert_called_with(0) - self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) - self._assertLogEntries([('e', "Material generic_pla_white has invalid version number one.")], _logentries) - - @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: []) - @patch("PyQt5.QtNetwork.QNetworkReply") - def test_sendMissingMaterials_WithMultipleLocalVersionsLowFirst(self, reply_mock): - reply_mock.attribute.return_value = 200 - reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTEMATERIAL_WHITE]).encode("ascii")) - - containerRegistry = ContainerRegistryMock() - localMaterialWhiteWithHigherVersion = self._LOCALMATERIAL_WHITE.copy() - localMaterialWhiteWithHigherVersion["version"] = "2" - containerRegistry.setContainersMetadata([self._LOCALMATERIAL_WHITE, localMaterialWhiteWithHigherVersion]) - - with mock.patch.object(Logger, "log", new=new_log): - with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): - SendMaterialJob(None).sendMissingMaterials(reply_mock) - - reply_mock.attribute.assert_called_with(0) - self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) - self._assertLogEntries([], _logentries) - - @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: []) - @patch("PyQt5.QtNetwork.QNetworkReply") - def test_sendMissingMaterials_MaterialMissingOnPrinter(self, reply_mock): - reply_mock.attribute.return_value = 200 - reply_mock.readAll.return_value = QByteArray( - json.dumps([self._REMOTEMATERIAL_WHITE]).encode("ascii")) - - containerRegistry = ContainerRegistryMock() - containerRegistry.setContainersMetadata([self._LOCALMATERIAL_WHITE, self._LOCALMATERIAL_BLACK]) - - with mock.patch.object(Logger, "log", new=new_log): - with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): - SendMaterialJob(None).sendMissingMaterials(reply_mock) - - reply_mock.attribute.assert_called_with(0) - self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) - self._assertLogEntries([], _logentries) - - @patch("builtins.open", lambda a, b: io.StringIO("")) - @patch("UM.MimeTypeDatabase.MimeTypeDatabase.getMimeTypeForFile", - lambda _: MimeType(name="application/x-ultimaker-material-profile", comment="Ultimaker Material Profile", - suffixes=["xml.fdm_material"])) - @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: ["/materials/generic_pla_white.xml.fdm_material"]) @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") - def test_sendMaterialsToPrinter(self, device_mock): - device_mock._createFormPart.return_value = "_xXx_" - with mock.patch.object(Logger, "log", new=new_log): - job = SendMaterialJob(device_mock) - job.sendMaterialsToPrinter({'generic_pla_white'}) - - self._assertLogEntries([("d", "Syncing material generic_pla_white with cluster.")], _logentries) - self.assertEqual([call._createFormPart('name="file"; filename="generic_pla_white.xml.fdm_material"', ''), - call.postFormWithParts(on_finished=job.sendingFinished, parts = ["_xXx_"], target = "materials/")], device_mock.method_calls) - @patch("PyQt5.QtNetwork.QNetworkReply") - def test_sendingFinished_success(self, reply_mock) -> None: + def test_sendMissingMaterials_withBadJsonAnswer(self, reply_mock, device_mock): reply_mock.attribute.return_value = 200 - with mock.patch.object(Logger, 'log', new=new_log): - SendMaterialJob(None).sendingFinished(reply_mock) - - reply_mock.attribute.assert_called_once_with(0) - self.assertEqual(0, len(_logentries)) - - @patch("PyQt5.QtNetwork.QNetworkReply") - def test_sendingFinished_failed(self, reply_mock) -> None: - reply_mock.attribute.return_value = 404 reply_mock.readAll.return_value = QByteArray(b'Six sick hicks nick six slick bricks with picks and sticks.') - - with mock.patch.object(Logger, 'log', new=new_log): - SendMaterialJob(None).sendingFinished(reply_mock) - + SendMaterialJob(device_mock).run() reply_mock.attribute.assert_called_with(0) - self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.attribute(0), call.readAll()]) + self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) + self.assertEqual(device_mock._onGetRemoteMaterials.method_calls, []) - self._assertLogEntries([ - ("e", "Received error code from printer when syncing material: 404"), - ("e", "Six sick hicks nick six slick bricks with picks and sticks.") - ], _logentries) - - @patch("PyQt5.QtNetwork.QNetworkReply") - def test_parseReply(self, reply_mock): - reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTEMATERIAL_WHITE]).encode("ascii")) - - response = SendMaterialJob._parseReply(reply_mock) - - self.assertTrue(len(response) == 1) - self.assertEqual(next(iter(response.values())), ClusterMaterial(**self._REMOTEMATERIAL_WHITE)) - - @patch("PyQt5.QtNetwork.QNetworkReply") - def test_parseReplyWithInvalidMaterial(self, reply_mock): - remoteMaterialWithInvalidVersion = self._REMOTEMATERIAL_WHITE.copy() - remoteMaterialWithInvalidVersion["version"] = "one" - reply_mock.readAll.return_value = QByteArray(json.dumps([remoteMaterialWithInvalidVersion]).encode("ascii")) - - with self.assertRaises(ValueError): - SendMaterialJob._parseReply(reply_mock) - - def test__getLocalMaterials(self): - containerRegistry = ContainerRegistryMock() - containerRegistry.setContainersMetadata([self._LOCALMATERIAL_WHITE, self._LOCALMATERIAL_BLACK]) - - with mock.patch.object(Logger, "log", new=new_log): - with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): - local_materials = SendMaterialJob(None)._getLocalMaterials() - - self.assertTrue(len(local_materials) == 2) - - def test__getLocalMaterialsWithMultipleVersions(self): - containerRegistry = ContainerRegistryMock() - localMaterialWithNewerVersion = self._LOCALMATERIAL_WHITE.copy() - localMaterialWithNewerVersion["version"] = 2 - containerRegistry.setContainersMetadata([self._LOCALMATERIAL_WHITE, localMaterialWithNewerVersion]) - - with mock.patch.object(Logger, "log", new=new_log): - with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): - local_materials = SendMaterialJob(None)._getLocalMaterials() - - self.assertTrue(len(local_materials) == 1) - self.assertTrue(list(local_materials.values())[0].version == 2) - - containerRegistry.setContainersMetadata([localMaterialWithNewerVersion, self._LOCALMATERIAL_WHITE]) - - with mock.patch.object(Logger, "log", new=new_log): - with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): - local_materials = SendMaterialJob(None)._getLocalMaterials() - - self.assertTrue(len(local_materials) == 1) - self.assertTrue(list(local_materials.values())[0].version == 2) - - def _assertLogEntries(self, first, second): - """ - Inspects the two sets of log entry tuples and fails when they are not the same - :param first: The first set of tuples - :param second: The second set of tuples - """ - self.assertEqual(len(first), len(second)) - - while len(first) > 0: - e1, m1 = first[0] - e2, m2 = second[0] - self.assertEqual(e1, e2) - self.assertEqual(m1, m2) - first.pop(0) - second.pop(0) + # @patch("PyQt5.QtNetwork.QNetworkReply") + # def test_sendMissingMaterials_withMissingGuid(self, reply_mock): + # reply_mock.attribute.return_value = 200 + # remoteMaterialWithoutGuid = self._REMOTEMATERIAL_WHITE.copy() + # del remoteMaterialWithoutGuid["guid"] + # reply_mock.readAll.return_value = QByteArray(json.dumps([remoteMaterialWithoutGuid]).encode("ascii")) + # + # with mock.patch.object(Logger, 'log', new=new_log): + # SendMaterialJob(None).sendMissingMaterials(reply_mock) + # + # reply_mock.attribute.assert_called_with(0) + # self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) + # self._assertLogEntries( + # [('e', "Request material storage on printer: Printer's answer was missing GUIDs.")], + # _logentries) + # + # @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: []) + # @patch("PyQt5.QtNetwork.QNetworkReply") + # def test_sendMissingMaterials_WithInvalidVersionInLocalMaterial(self, reply_mock): + # reply_mock.attribute.return_value = 200 + # reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTEMATERIAL_WHITE]).encode("ascii")) + # + # containerRegistry = ContainerRegistryMock() + # localMaterialWhiteWithInvalidVersion = self._LOCALMATERIAL_WHITE.copy() + # localMaterialWhiteWithInvalidVersion["version"] = "one" + # containerRegistry.setContainersMetadata([localMaterialWhiteWithInvalidVersion]) + # + # with mock.patch.object(Logger, "log", new=new_log): + # with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): + # SendMaterialJob(None).sendMissingMaterials(reply_mock) + # + # reply_mock.attribute.assert_called_with(0) + # self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) + # self._assertLogEntries([('e', "Material generic_pla_white has invalid version number one.")], _logentries) + # + # @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: []) + # @patch("PyQt5.QtNetwork.QNetworkReply") + # def test_sendMissingMaterials_WithMultipleLocalVersionsLowFirst(self, reply_mock): + # reply_mock.attribute.return_value = 200 + # reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTEMATERIAL_WHITE]).encode("ascii")) + # + # containerRegistry = ContainerRegistryMock() + # localMaterialWhiteWithHigherVersion = self._LOCALMATERIAL_WHITE.copy() + # localMaterialWhiteWithHigherVersion["version"] = "2" + # containerRegistry.setContainersMetadata([self._LOCALMATERIAL_WHITE, localMaterialWhiteWithHigherVersion]) + # + # with mock.patch.object(Logger, "log", new=new_log): + # with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): + # SendMaterialJob(None).sendMissingMaterials(reply_mock) + # + # reply_mock.attribute.assert_called_with(0) + # self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) + # self._assertLogEntries([], _logentries) + # + # @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: []) + # @patch("PyQt5.QtNetwork.QNetworkReply") + # def test_sendMissingMaterials_MaterialMissingOnPrinter(self, reply_mock): + # reply_mock.attribute.return_value = 200 + # reply_mock.readAll.return_value = QByteArray( + # json.dumps([self._REMOTEMATERIAL_WHITE]).encode("ascii")) + # + # containerRegistry = ContainerRegistryMock() + # containerRegistry.setContainersMetadata([self._LOCALMATERIAL_WHITE, self._LOCALMATERIAL_BLACK]) + # + # with mock.patch.object(Logger, "log", new=new_log): + # with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): + # SendMaterialJob(None).sendMissingMaterials(reply_mock) + # + # reply_mock.attribute.assert_called_with(0) + # self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) + # self._assertLogEntries([], _logentries) + # + # @patch("builtins.open", lambda a, b: io.StringIO("")) + # @patch("UM.MimeTypeDatabase.MimeTypeDatabase.getMimeTypeForFile", + # lambda _: MimeType(name="application/x-ultimaker-material-profile", comment="Ultimaker Material Profile", + # suffixes=["xml.fdm_material"])) + # @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: ["/materials/generic_pla_white.xml.fdm_material"]) + # @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") + # def test_sendMaterialsToPrinter(self, device_mock): + # device_mock._createFormPart.return_value = "_xXx_" + # with mock.patch.object(Logger, "log", new=new_log): + # job = SendMaterialJob(device_mock) + # job.sendMaterialsToPrinter({'generic_pla_white'}) + # + # self._assertLogEntries([("d", "Syncing material generic_pla_white with cluster.")], _logentries) + # self.assertEqual([call._createFormPart('name="file"; filename="generic_pla_white.xml.fdm_material"', ''), + # call.postFormWithParts(on_finished=job.sendingFinished, parts = ["_xXx_"], target = "materials/")], device_mock.method_calls) + # + # @patch("PyQt5.QtNetwork.QNetworkReply") + # def test_sendingFinished_success(self, reply_mock) -> None: + # reply_mock.attribute.return_value = 200 + # with mock.patch.object(Logger, 'log', new=new_log): + # SendMaterialJob(None).sendingFinished(reply_mock) + # + # reply_mock.attribute.assert_called_once_with(0) + # self.assertEqual(0, len(_logentries)) + # + # @patch("PyQt5.QtNetwork.QNetworkReply") + # def test_sendingFinished_failed(self, reply_mock) -> None: + # reply_mock.attribute.return_value = 404 + # reply_mock.readAll.return_value = QByteArray(b'Six sick hicks nick six slick bricks with picks and sticks.') + # + # with mock.patch.object(Logger, 'log', new=new_log): + # SendMaterialJob(None).sendingFinished(reply_mock) + # + # reply_mock.attribute.assert_called_with(0) + # self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.attribute(0), call.readAll()]) + # + # self._assertLogEntries([ + # ("e", "Received error code from printer when syncing material: 404"), + # ("e", "Six sick hicks nick six slick bricks with picks and sticks.") + # ], _logentries) + # + # @patch("PyQt5.QtNetwork.QNetworkReply") + # def test_parseReply(self, reply_mock): + # reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTEMATERIAL_WHITE]).encode("ascii")) + # + # response = SendMaterialJob._parseReply(reply_mock) + # + # self.assertTrue(len(response) == 1) + # self.assertEqual(next(iter(response.values())), ClusterMaterial(**self._REMOTEMATERIAL_WHITE)) + # + # @patch("PyQt5.QtNetwork.QNetworkReply") + # def test_parseReplyWithInvalidMaterial(self, reply_mock): + # remoteMaterialWithInvalidVersion = self._REMOTEMATERIAL_WHITE.copy() + # remoteMaterialWithInvalidVersion["version"] = "one" + # reply_mock.readAll.return_value = QByteArray(json.dumps([remoteMaterialWithInvalidVersion]).encode("ascii")) + # + # with self.assertRaises(ValueError): + # SendMaterialJob._parseReply(reply_mock) + # + # def test__getLocalMaterials(self): + # containerRegistry = ContainerRegistryMock() + # containerRegistry.setContainersMetadata([self._LOCALMATERIAL_WHITE, self._LOCALMATERIAL_BLACK]) + # + # with mock.patch.object(Logger, "log", new=new_log): + # with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): + # local_materials = SendMaterialJob(None)._getLocalMaterials() + # + # self.assertTrue(len(local_materials) == 2) + # + # def test__getLocalMaterialsWithMultipleVersions(self): + # containerRegistry = ContainerRegistryMock() + # localMaterialWithNewerVersion = self._LOCALMATERIAL_WHITE.copy() + # localMaterialWithNewerVersion["version"] = 2 + # containerRegistry.setContainersMetadata([self._LOCALMATERIAL_WHITE, localMaterialWithNewerVersion]) + # + # with mock.patch.object(Logger, "log", new=new_log): + # with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): + # local_materials = SendMaterialJob(None)._getLocalMaterials() + # + # self.assertTrue(len(local_materials) == 1) + # self.assertTrue(list(local_materials.values())[0].version == 2) + # + # containerRegistry.setContainersMetadata([localMaterialWithNewerVersion, self._LOCALMATERIAL_WHITE]) + # + # with mock.patch.object(Logger, "log", new=new_log): + # with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): + # local_materials = SendMaterialJob(None)._getLocalMaterials() + # + # self.assertTrue(len(local_materials) == 1) + # self.assertTrue(list(local_materials.values())[0].version == 2) + # + # def _assertLogEntries(self, first, second): + # """ + # Inspects the two sets of log entry tuples and fails when they are not the same + # :param first: The first set of tuples + # :param second: The second set of tuples + # """ + # self.assertEqual(len(first), len(second)) + # + # while len(first) > 0: + # e1, m1 = first[0] + # e2, m2 = second[0] + # self.assertEqual(e1, e2) + # self.assertEqual(m1, m2) + # first.pop(0) + # second.pop(0) From 2f624cc78f18baf189e3d39c928ebc605746a6c1 Mon Sep 17 00:00:00 2001 From: Dario Minnucci Date: Mon, 19 Nov 2018 12:37:57 +0100 Subject: [PATCH 030/146] Add BIBO2 printer (dual and single extruder settings) --- resources/definitions/bibo2_dual.def.json | 98 +++++++++++++++++++ .../bibo2_single_extruder_0.def.json | 98 +++++++++++++++++++ .../bibo2_single_extruder_1.def.json | 98 +++++++++++++++++++ .../extruders/bibo2_dual_extruder_0.def.json | 40 ++++++++ .../extruders/bibo2_dual_extruder_1.def.json | 40 ++++++++ .../bibo2_single_extruder_0_0.def.json | 40 ++++++++ .../bibo2_single_extruder_0_1.def.json | 40 ++++++++ .../bibo2_single_extruder_1_0.def.json | 40 ++++++++ .../bibo2_single_extruder_1_1.def.json | 40 ++++++++ 9 files changed, 534 insertions(+) create mode 100644 resources/definitions/bibo2_dual.def.json create mode 100644 resources/definitions/bibo2_single_extruder_0.def.json create mode 100644 resources/definitions/bibo2_single_extruder_1.def.json create mode 100644 resources/extruders/bibo2_dual_extruder_0.def.json create mode 100644 resources/extruders/bibo2_dual_extruder_1.def.json create mode 100644 resources/extruders/bibo2_single_extruder_0_0.def.json create mode 100644 resources/extruders/bibo2_single_extruder_0_1.def.json create mode 100644 resources/extruders/bibo2_single_extruder_1_0.def.json create mode 100644 resources/extruders/bibo2_single_extruder_1_1.def.json diff --git a/resources/definitions/bibo2_dual.def.json b/resources/definitions/bibo2_dual.def.json new file mode 100644 index 0000000000..e57fb64ec0 --- /dev/null +++ b/resources/definitions/bibo2_dual.def.json @@ -0,0 +1,98 @@ +{ + "id": "BIBO2 dual", + "version": 2, + "name": "BIBO2 dual", + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "na", + "manufacturer": "BIBO", + "category": "Other", + "file_formats": "text/x-gcode", + "has_materials": true, + "machine_extruder_trains": { + "0": "bibo2_dual_extruder_0", + "1": "bibo2_dual_extruder_1" + }, + "first_start_actions": [ + "MachineSettingsAction" + ] + }, + "overrides": { + "machine_name": { + "default_value": "BIBO2 dual" + }, + "machine_width": { + "default_value": 214 + }, + "machine_height": { + "default_value": 160 + }, + "machine_depth": { + "default_value": 186 + }, + "machine_center_is_zero": { + "default_value": true + }, + "machine_heated_bed": { + "default_value": true + }, + "machine_nozzle_size": { + "default_value": 0.4 + }, + "machine_nozzle_heat_up_speed": { + "default_value": 2 + }, + "machine_nozzle_cool_down_speed": { + "default_value": 2 + }, + "machine_head_with_fans_polygon": { + "default_value": [ + [ + -68.18, + 64.63 + ], + [ + -68.18, + -47.38 + ], + [ + 35.18, + 64.63 + ], + [ + 35.18, + -47.38 + ] + ] + }, + "material_diameter": { + "default_value": 1.75 + }, + "gantry_height": { + "default_value": 12 + }, + "machine_use_extruder_offset_to_offset_coords": { + "default_value": true + }, + "machine_gcode_flavor": { + "default_value": "RepRap (Marlin/Sprinter)" + }, + "machine_start_gcode": { + "default_value": "G21 ;metric values\nG90 ;absolute positioning\nM107 ;start with the fan off\nG28 X0 Y0 ;move X/Y to min endstops\nG28 Z0 ;move Z to min endstops\nG1 Z2.0 F400 ;move the platform down 15mm\nT0\nG92 E0\nG28\nG1 Y0 F1200 E0\nG92 E0\nM117 BIBO Printing..." + }, + "machine_end_gcode": { + "default_value": ";End GCode\nM104 T0 S0 ;extruder heater off\nM104 T1 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nG91\nG1 Z1 F100 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-2 X-20 Y-20 F300 ;move Z up a bit and retract filament even more\nG28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning" + }, + "machine_extruder_count": { + "default_value": 2 + }, + "prime_tower_position_x": { + "default_value": 50 + }, + "prime_tower_position_y": { + "default_value": 50 + } + } +} + diff --git a/resources/definitions/bibo2_single_extruder_0.def.json b/resources/definitions/bibo2_single_extruder_0.def.json new file mode 100644 index 0000000000..93c7a4e5ae --- /dev/null +++ b/resources/definitions/bibo2_single_extruder_0.def.json @@ -0,0 +1,98 @@ +{ + "id": "BIBO2 single E1", + "version": 2, + "name": "BIBO2 single E1", + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "na", + "manufacturer": "BIBO", + "category": "Other", + "file_formats": "text/x-gcode", + "has_materials": true, + "machine_extruder_trains": { + "0": "bibo2_single_extruder_0_0", + "1": "bibo2_single_extruder_0_1" + }, + "first_start_actions": [ + "MachineSettingsAction" + ] + }, + "overrides": { + "machine_name": { + "default_value": "BIBO2 single Extruder 1 (right)" + }, + "machine_width": { + "default_value": 214 + }, + "machine_height": { + "default_value": 160 + }, + "machine_depth": { + "default_value": 186 + }, + "machine_center_is_zero": { + "default_value": true + }, + "machine_heated_bed": { + "default_value": true + }, + "machine_nozzle_size": { + "default_value": 0.4 + }, + "machine_nozzle_heat_up_speed": { + "default_value": 2 + }, + "machine_nozzle_cool_down_speed": { + "default_value": 2 + }, + "machine_head_with_fans_polygon": { + "default_value": [ + [ + -68.18, + 64.63 + ], + [ + -68.18, + -47.38 + ], + [ + 35.18, + 64.63 + ], + [ + 35.18, + -47.38 + ] + ] + }, + "material_diameter": { + "default_value": 1.75 + }, + "gantry_height": { + "default_value": 12 + }, + "machine_use_extruder_offset_to_offset_coords": { + "default_value": true + }, + "machine_gcode_flavor": { + "default_value": "RepRap (Marlin/Sprinter)" + }, + "machine_start_gcode": { + "default_value": ";Startcode BIBO printers\nM109 T1 S170 ;preheat the other extruder, so it will not knock or ruin the print\nG90 ; absolute mode\nG21 ; metric values\nM82 ; Extruder in absolute mode\nM107\nG28\nG1 Z2 F400\nT0\nG90\nG92 E0\nG28\nG1 Y0 F1200 E0\nG92 E0\nG1 X-15.0 Y-92.9 Z0.3 F2400.0\t\t; move to start-line position\nG1 X15.0 Y-92.9 Z0.3 F1000.0 E2\t\t; draw 1st line\nG1 X15.0 Y-92.6 Z0.3 F3000.0\t\t; move to side a little\nG1 X-15.0 Y-92.6 Z0.3 F1000.0 E4\t\t; draw 2nd line\nG1 X-15.0 Y-92.3 Z0.3 F3000.0\t\t; move to side a little\nG1 X15.0 Y-92.3 Z0.3 F1000.0 E6\t\t; draw 3rd line\nG1 X15.0 Y-92 Z0.3 F3000.0\t\t; move to side a little\nG1 X-15.0 Y-92 Z0.3 F1000.0 E8\t\t; draw 4th line\nG1 X-16.0 Y-91.7 Z0.3 F3000.0\t\t; move to side a little\nG1 X16.0 Y-91.7 Z0.3 F1000.0 E10\t\t; draw 5th line\nG1 X16.0 Y-91.4 Z0.3 F3000.0\t\t; move to side a little\nG1 X-16.0 Y-91.4 Z0.3 F1000.0 E12\t\t; draw 5th line\nG1 E11.5 F2400\t\t\t\t; retract filament 0.5mm\nG92 E0\nM117 BIBO Printing..." + }, + "machine_end_gcode": { + "default_value": ";BIBO End GCode\nM107\nG91 ; Relative positioning\nG1 Z1 F100\nM104 T0 S0\nM104 T1 S0\nG1 X-20 Y-20 F3000\nG28 X0 Y0\nG90 ; Absolute positioning\nG92 E0 ; Reset extruder position\nM140 S0 ; Disable heated bed\nM84 ; Turn steppers off\nM117 BIBO Print complete\n " + }, + "machine_extruder_count": { + "default_value": 2 + }, + "prime_tower_position_x": { + "default_value": 50 + }, + "prime_tower_position_y": { + "default_value": 50 + } + } +} + diff --git a/resources/definitions/bibo2_single_extruder_1.def.json b/resources/definitions/bibo2_single_extruder_1.def.json new file mode 100644 index 0000000000..246add09ab --- /dev/null +++ b/resources/definitions/bibo2_single_extruder_1.def.json @@ -0,0 +1,98 @@ +{ + "id": "BIBO2 single E2", + "version": 2, + "name": "BIBO2 single E2", + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "na", + "manufacturer": "BIBO", + "category": "Other", + "file_formats": "text/x-gcode", + "has_materials": true, + "machine_extruder_trains": { + "0": "bibo2_single_extruder_1_0", + "1": "bibo2_single_extruder_1_1" + }, + "first_start_actions": [ + "MachineSettingsAction" + ] + }, + "overrides": { + "machine_name": { + "default_value": "BIBO2 single Extruder 2 (left)" + }, + "machine_width": { + "default_value": 214 + }, + "machine_height": { + "default_value": 160 + }, + "machine_depth": { + "default_value": 186 + }, + "machine_center_is_zero": { + "default_value": true + }, + "machine_heated_bed": { + "default_value": true + }, + "machine_nozzle_size": { + "default_value": 0.4 + }, + "machine_nozzle_heat_up_speed": { + "default_value": 2 + }, + "machine_nozzle_cool_down_speed": { + "default_value": 2 + }, + "machine_head_with_fans_polygon": { + "default_value": [ + [ + -68.18, + 64.63 + ], + [ + -68.18, + -47.38 + ], + [ + 35.18, + 64.63 + ], + [ + 35.18, + -47.38 + ] + ] + }, + "material_diameter": { + "default_value": 1.75 + }, + "gantry_height": { + "default_value": 12 + }, + "machine_use_extruder_offset_to_offset_coords": { + "default_value": true + }, + "machine_gcode_flavor": { + "default_value": "RepRap (Marlin/Sprinter)" + }, + "machine_start_gcode": { + "default_value": ";Startcode BIBO printers\nM109 T0 S170 ;preheat the other extruder, so it will not knock or ruin the print\nG90 ; absolute mode\nG21 ; metric values\nM82 ; Extruder in absolute mode\nM107\nG28\nG1 Z2 F400\nT0\nG90\nG92 E0\nG28\nG1 Y0 F1200 E0\nG92 E0\nT1\nG92 E0\nG1 X-15.0 Y-92.9 Z0.3 F2400.0\t\t; move to start-line position\nG1 X15.0 Y-92.9 Z0.3 F1000.0 E2\t\t; draw 1st line\nG1 X15.0 Y-92.6 Z0.3 F3000.0\t\t; move to side a little\nG1 X-15.0 Y-92.6 Z0.3 F1000.0 E4\t\t; draw 2nd line\nG1 X-15.0 Y-92.3 Z0.3 F3000.0\t\t; move to side a little\nG1 X15.0 Y-92.3 Z0.3 F1000.0 E6\t\t; draw 3rd line\nG1 X15.0 Y-92 Z0.3 F3000.0\t\t; move to side a little\nG1 X-15.0 Y-92 Z0.3 F1000.0 E8\t\t; draw 4th line\nG1 X-16.0 Y-91.7 Z0.3 F3000.0\t\t; move to side a little\nG1 X16.0 Y-91.7 Z0.3 F1000.0 E10\t\t; draw 5th line\nG1 X16.0 Y-91.4 Z0.3 F3000.0\t\t; move to side a little\nG1 X-16.0 Y-91.4 Z0.3 F1000.0 E12\t\t; draw 5th line\nG1 E11.5 F2400\t\t\t\t; retract filament 0.5mm\nG92 E0\nM117 BIBO Printing..." + }, + "machine_end_gcode": { + "default_value": ";BIBO End GCode\nM107\nG91 ; Relative positioning\nG1 Z1 F100\nM104 T0 S0\nM104 T1 S0\nG1 X-20 Y-20 F3000\nG28 X0 Y0\nG90 ; Absolute positioning\nG92 E0 ; Reset extruder position\nM140 S0 ; Disable heated bed\nM84 ; Turn steppers off\nM117 BIBO Print complete\n " + }, + "machine_extruder_count": { + "default_value": 2 + }, + "prime_tower_position_x": { + "default_value": 50 + }, + "prime_tower_position_y": { + "default_value": 50 + } + } +} + diff --git a/resources/extruders/bibo2_dual_extruder_0.def.json b/resources/extruders/bibo2_dual_extruder_0.def.json new file mode 100644 index 0000000000..7cdc03d504 --- /dev/null +++ b/resources/extruders/bibo2_dual_extruder_0.def.json @@ -0,0 +1,40 @@ +{ + "id": "BIBO1", + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "BIBO2 dual", + "position": "0" + }, + "overrides": { + "extruder_nr": { + "default_value": 0, + "maximum_value": "1" + }, + "machine_nozzle_offset_x": { + "default_value": 0.0 + }, + "machine_nozzle_offset_y": { + "default_value": 0.0 + }, + "machine_extruder_start_pos_abs": { + "default_value": true + }, + "machine_extruder_start_pos_x": { + "value": "prime_tower_position_x" + }, + "machine_extruder_start_pos_y": { + "value": "prime_tower_position_y" + }, + "machine_extruder_end_pos_abs": { + "default_value": true + }, + "machine_extruder_end_pos_x": { + "value": "prime_tower_position_x" + }, + "machine_extruder_end_pos_y": { + "value": "prime_tower_position_y" + } + } +} diff --git a/resources/extruders/bibo2_dual_extruder_1.def.json b/resources/extruders/bibo2_dual_extruder_1.def.json new file mode 100644 index 0000000000..daa1504220 --- /dev/null +++ b/resources/extruders/bibo2_dual_extruder_1.def.json @@ -0,0 +1,40 @@ +{ + "id": "BIBO2", + "version": 2, + "name": "Extruder 2", + "inherits": "fdmextruder", + "metadata": { + "machine": "BIBO2 dual", + "position": "1" + }, + "overrides": { + "extruder_nr": { + "default_value": 1, + "maximum_value": "1" + }, + "machine_nozzle_offset_x": { + "default_value": 0 + }, + "machine_nozzle_offset_y": { + "default_value": 0 + }, + "machine_extruder_start_pos_abs": { + "default_value": true + }, + "machine_extruder_start_pos_x": { + "value": "prime_tower_position_x" + }, + "machine_extruder_start_pos_y": { + "value": "prime_tower_position_y" + }, + "machine_extruder_end_pos_abs": { + "default_value": true + }, + "machine_extruder_end_pos_x": { + "value": "prime_tower_position_x" + }, + "machine_extruder_end_pos_y": { + "value": "prime_tower_position_y" + } + } +} diff --git a/resources/extruders/bibo2_single_extruder_0_0.def.json b/resources/extruders/bibo2_single_extruder_0_0.def.json new file mode 100644 index 0000000000..7d0b246131 --- /dev/null +++ b/resources/extruders/bibo2_single_extruder_0_0.def.json @@ -0,0 +1,40 @@ +{ + "id": "BIBO2 E1a", + "version": 2, + "name": "BIBO2 E1", + "inherits": "fdmextruder", + "metadata": { + "machine": "BIBO2 single E1", + "position": "0" + }, + "overrides": { + "extruder_nr": { + "default_value": 0, + "maximum_value": "1" + }, + "machine_nozzle_offset_x": { + "default_value": 0.0 + }, + "machine_nozzle_offset_y": { + "default_value": 0.0 + }, + "machine_extruder_start_pos_abs": { + "default_value": true + }, + "machine_extruder_start_pos_x": { + "value": "prime_tower_position_x" + }, + "machine_extruder_start_pos_y": { + "value": "prime_tower_position_y" + }, + "machine_extruder_end_pos_abs": { + "default_value": true + }, + "machine_extruder_end_pos_x": { + "value": "prime_tower_position_x" + }, + "machine_extruder_end_pos_y": { + "value": "prime_tower_position_y" + } + } +} diff --git a/resources/extruders/bibo2_single_extruder_0_1.def.json b/resources/extruders/bibo2_single_extruder_0_1.def.json new file mode 100644 index 0000000000..76187696fc --- /dev/null +++ b/resources/extruders/bibo2_single_extruder_0_1.def.json @@ -0,0 +1,40 @@ +{ + "id": "BIBO2 E1b", + "version": 2, + "name": "E2 not used", + "inherits": "fdmextruder", + "metadata": { + "machine": "BIBO2 single E1", + "position": "1" + }, + "overrides": { + "extruder_nr": { + "default_value": 1, + "maximum_value": "1" + }, + "machine_nozzle_offset_x": { + "default_value": 0.0 + }, + "machine_nozzle_offset_y": { + "default_value": 0.0 + }, + "machine_extruder_start_pos_abs": { + "default_value": true + }, + "machine_extruder_start_pos_x": { + "value": "prime_tower_position_x" + }, + "machine_extruder_start_pos_y": { + "value": "prime_tower_position_y" + }, + "machine_extruder_end_pos_abs": { + "default_value": true + }, + "machine_extruder_end_pos_x": { + "value": "prime_tower_position_x" + }, + "machine_extruder_end_pos_y": { + "value": "prime_tower_position_y" + } + } +} diff --git a/resources/extruders/bibo2_single_extruder_1_0.def.json b/resources/extruders/bibo2_single_extruder_1_0.def.json new file mode 100644 index 0000000000..3cf667de82 --- /dev/null +++ b/resources/extruders/bibo2_single_extruder_1_0.def.json @@ -0,0 +1,40 @@ +{ + "id": "BIBO2 E2a", + "version": 2, + "name": "E1 not used", + "inherits": "fdmextruder", + "metadata": { + "machine": "BIBO2 single E2", + "position": "0" + }, + "overrides": { + "extruder_nr": { + "default_value": 0, + "maximum_value": "1" + }, + "machine_nozzle_offset_x": { + "default_value": 0 + }, + "machine_nozzle_offset_y": { + "default_value": 0 + }, + "machine_extruder_start_pos_abs": { + "default_value": true + }, + "machine_extruder_start_pos_x": { + "value": "prime_tower_position_x" + }, + "machine_extruder_start_pos_y": { + "value": "prime_tower_position_y" + }, + "machine_extruder_end_pos_abs": { + "default_value": true + }, + "machine_extruder_end_pos_x": { + "value": "prime_tower_position_x" + }, + "machine_extruder_end_pos_y": { + "value": "prime_tower_position_y" + } + } +} diff --git a/resources/extruders/bibo2_single_extruder_1_1.def.json b/resources/extruders/bibo2_single_extruder_1_1.def.json new file mode 100644 index 0000000000..e8f3ec7054 --- /dev/null +++ b/resources/extruders/bibo2_single_extruder_1_1.def.json @@ -0,0 +1,40 @@ +{ + "id": "BIBO2 E2b", + "version": 2, + "name": "BIBO2 E2", + "inherits": "fdmextruder", + "metadata": { + "machine": "BIBO2 single E2", + "position": "1" + }, + "overrides": { + "extruder_nr": { + "default_value": 1, + "maximum_value": "1" + }, + "machine_nozzle_offset_x": { + "default_value": 0 + }, + "machine_nozzle_offset_y": { + "default_value": 0 + }, + "machine_extruder_start_pos_abs": { + "default_value": true + }, + "machine_extruder_start_pos_x": { + "value": "prime_tower_position_x" + }, + "machine_extruder_start_pos_y": { + "value": "prime_tower_position_y" + }, + "machine_extruder_end_pos_abs": { + "default_value": true + }, + "machine_extruder_end_pos_x": { + "value": "prime_tower_position_x" + }, + "machine_extruder_end_pos_y": { + "value": "prime_tower_position_y" + } + } +} From f8f133d2ef92dfabb636eac70b09e681bd4b4d63 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 19 Nov 2018 13:42:28 +0100 Subject: [PATCH 031/146] Fix running tests in plugin using pytest --- .../tests/TestLegacyProfileReader.py | 9 +++++---- .../tests/TestVersionUpgrade25to26.py | 11 ++++++++--- .../tests/TestVersionUpgrade26to27.py | 9 +++++---- .../tests/TestVersionUpgrade27to30.py | 8 ++++---- .../tests/TestVersionUpgrade34to35.py | 10 ++++++---- 5 files changed, 28 insertions(+), 19 deletions(-) diff --git a/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py b/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py index 480a61f301..3c9e46b6d8 100644 --- a/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py +++ b/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py @@ -1,8 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. - import configparser # An input for some functions we're testing. -import os.path # To find the integration test .ini files. +import os # To find the integration test .ini files. import pytest # To register tests with. import unittest.mock # To mock the application, plug-in and container registry out. @@ -11,13 +10,15 @@ import UM.PluginRegistry # To mock the plug-in registry out. import UM.Settings.ContainerRegistry # To mock the container registry out. import UM.Settings.InstanceContainer # To intercept the serialised data from the read() function. -import LegacyProfileReader as LegacyProfileReaderModule # To get the directory of the module. -from LegacyProfileReader import LegacyProfileReader # The module we're testing. +import plugins.LegacyProfileReader.LegacyProfileReader as LegacyProfileReaderModule +from plugins.LegacyProfileReader.LegacyProfileReader import LegacyProfileReader + @pytest.fixture def legacy_profile_reader(): return LegacyProfileReader() + test_prepareDefaultsData = [ { "defaults": diff --git a/plugins/VersionUpgrade/VersionUpgrade25to26/tests/TestVersionUpgrade25to26.py b/plugins/VersionUpgrade/VersionUpgrade25to26/tests/TestVersionUpgrade25to26.py index 9d7c7646cc..588c0cb3db 100644 --- a/plugins/VersionUpgrade/VersionUpgrade25to26/tests/TestVersionUpgrade25to26.py +++ b/plugins/VersionUpgrade/VersionUpgrade25to26/tests/TestVersionUpgrade25to26.py @@ -1,16 +1,17 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import configparser +import pytest -import configparser #To check whether the appropriate exceptions are raised. -import pytest #To register tests with. +from plugins.VersionUpgrade.VersionUpgrade25to26 import VersionUpgrade25to26 -import VersionUpgrade25to26 #The module we're testing. ## Creates an instance of the upgrader to test with. @pytest.fixture def upgrader(): return VersionUpgrade25to26.VersionUpgrade25to26() + test_cfg_version_good_data = [ { "test_name": "Simple", @@ -60,6 +61,7 @@ setting_version = -3 } ] + ## Tests the technique that gets the version number from CFG files. # # \param data The parametrised data to test with. It contains a test name @@ -116,6 +118,7 @@ version = 1.2 } ] + ## Tests whether getting a version number from bad CFG files gives an # exception. # @@ -155,6 +158,7 @@ foo = bar } ] + ## Tests whether the settings that should be removed are removed for the 2.6 # version of preferences. @pytest.mark.parametrize("data", test_upgrade_preferences_removed_settings_data) @@ -200,6 +204,7 @@ type = instance_container } ] + ## Tests whether the settings that should be removed are removed for the 2.6 # version of instance containers. @pytest.mark.parametrize("data", test_upgrade_instance_container_removed_settings_data) diff --git a/plugins/VersionUpgrade/VersionUpgrade26to27/tests/TestVersionUpgrade26to27.py b/plugins/VersionUpgrade/VersionUpgrade26to27/tests/TestVersionUpgrade26to27.py index eebaca23c6..45d41e7a1b 100644 --- a/plugins/VersionUpgrade/VersionUpgrade26to27/tests/TestVersionUpgrade26to27.py +++ b/plugins/VersionUpgrade/VersionUpgrade26to27/tests/TestVersionUpgrade26to27.py @@ -1,15 +1,16 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import configparser +import pytest -import configparser #To check whether the appropriate exceptions are raised. -import pytest #To register tests with. +from plugins.VersionUpgrade.VersionUpgrade26to27.VersionUpgrade26to27 import VersionUpgrade26to27 -import VersionUpgrade26to27 #The module we're testing. ## Creates an instance of the upgrader to test with. @pytest.fixture def upgrader(): - return VersionUpgrade26to27.VersionUpgrade26to27() + return VersionUpgrade26to27() + test_cfg_version_good_data = [ { diff --git a/plugins/VersionUpgrade/VersionUpgrade27to30/tests/TestVersionUpgrade27to30.py b/plugins/VersionUpgrade/VersionUpgrade27to30/tests/TestVersionUpgrade27to30.py index cae08ebcfd..7b77b85993 100644 --- a/plugins/VersionUpgrade/VersionUpgrade27to30/tests/TestVersionUpgrade27to30.py +++ b/plugins/VersionUpgrade/VersionUpgrade27to30/tests/TestVersionUpgrade27to30.py @@ -1,15 +1,15 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import configparser +import pytest -import configparser #To parse the resulting config files. -import pytest #To register tests with. +from plugins.VersionUpgrade.VersionUpgrade27to30.VersionUpgrade27to30 import VersionUpgrade27to30 -import VersionUpgrade27to30 #The module we're testing. ## Creates an instance of the upgrader to test with. @pytest.fixture def upgrader(): - return VersionUpgrade27to30.VersionUpgrade27to30() + return VersionUpgrade27to30() test_cfg_version_good_data = [ { diff --git a/plugins/VersionUpgrade/VersionUpgrade34to35/tests/TestVersionUpgrade34to35.py b/plugins/VersionUpgrade/VersionUpgrade34to35/tests/TestVersionUpgrade34to35.py index b74e6f35ac..4f77fcd093 100644 --- a/plugins/VersionUpgrade/VersionUpgrade34to35/tests/TestVersionUpgrade34to35.py +++ b/plugins/VersionUpgrade/VersionUpgrade34to35/tests/TestVersionUpgrade34to35.py @@ -1,15 +1,16 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import configparser +import pytest -import configparser #To parse the resulting config files. -import pytest #To register tests with. +from plugins.VersionUpgrade.VersionUpgrade34to35.VersionUpgrade34to35 import VersionUpgrade34to35 -import VersionUpgrade34to35 #The module we're testing. ## Creates an instance of the upgrader to test with. @pytest.fixture def upgrader(): - return VersionUpgrade34to35.VersionUpgrade34to35() + return VersionUpgrade34to35() + test_upgrade_version_nr_data = [ ("Empty config file", @@ -25,6 +26,7 @@ test_upgrade_version_nr_data = [ ) ] + ## Tests whether the version numbers are updated. @pytest.mark.parametrize("test_name, file_data", test_upgrade_version_nr_data) def test_upgradeVersionNr(test_name, file_data, upgrader): From dc17bd849968d180dc061aff6ca7eaed53bfcff1 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 19 Nov 2018 13:54:45 +0100 Subject: [PATCH 032/146] Fix the first tests --- .../UM3NetworkPrinting/src/SendMaterialJob.py | 1 + .../tests/TestSendMaterialJob.py | 23 ++++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py index 349e6929ff..572558e352 100644 --- a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py @@ -157,6 +157,7 @@ class SendMaterialJob(Job): @classmethod def _parseReply(cls, reply: QNetworkReply) -> Dict[str, ClusterMaterial]: remote_materials_list = json.loads(reply.readAll().data().decode("utf-8")) + print("remote_materials_list", remote_materials_list) return {material["guid"]: ClusterMaterial(**material) for material in remote_materials_list} ## Retrieves a list of local materials diff --git a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py index bc6e1def14..b51d978ed2 100644 --- a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py @@ -90,26 +90,33 @@ class TestSendMaterialJob(TestCase): def test_run(self, device_mock): job = SendMaterialJob(device_mock) job.run() + + # We expect the materials endpoint to be called when the job runs. device_mock.get.assert_called_with("materials/", on_finished=job._onGetRemoteMaterials) @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") @patch("PyQt5.QtNetwork.QNetworkReply") def test_sendMissingMaterials_withFailedRequest(self, reply_mock, device_mock): reply_mock.attribute.return_value = 404 - SendMaterialJob(device_mock).run() - reply_mock.attribute.assert_called_with(0) - self.assertEqual(reply_mock.method_calls, [call.attribute(0)]) - self.assertEqual(device_mock._onGetRemoteMaterials.method_calls, []) + job = SendMaterialJob(device_mock) + job._onGetRemoteMaterials(reply_mock) + + # We expect the error string to be retrieved and the device not to be called for any follow up. + self.assertEqual([call.attribute(0), call.errorString()], reply_mock.method_calls) + self.assertEqual([], device_mock.method_calls) @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") @patch("PyQt5.QtNetwork.QNetworkReply") def test_sendMissingMaterials_withBadJsonAnswer(self, reply_mock, device_mock): reply_mock.attribute.return_value = 200 reply_mock.readAll.return_value = QByteArray(b'Six sick hicks nick six slick bricks with picks and sticks.') - SendMaterialJob(device_mock).run() - reply_mock.attribute.assert_called_with(0) - self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) - self.assertEqual(device_mock._onGetRemoteMaterials.method_calls, []) + job = SendMaterialJob(device_mock) + job._onGetRemoteMaterials(reply_mock) + + # We expect the reply to be called once to try to get the printers from the list (readAll()). + # Given that the parsing there fails we do no expect the device to be called for any follow up. + self.assertEqual([call.attribute(0), call.readAll()], reply_mock.method_calls) + self.assertEqual([], device_mock.method_calls) # @patch("PyQt5.QtNetwork.QNetworkReply") # def test_sendMissingMaterials_withMissingGuid(self, reply_mock): From 0b1ac87354d53457dcfba0f927e749639a7600a0 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 19 Nov 2018 15:03:43 +0100 Subject: [PATCH 033/146] Fix some formatting, cleanup import --- .../UM3NetworkPrinting/src/SendMaterialJob.py | 9 +++-- .../tests/TestSendMaterialJob.py | 36 ++++++++----------- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py index 572558e352..62414763b6 100644 --- a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py @@ -48,10 +48,10 @@ class SendMaterialJob(Job): try: remote_materials_by_guid = self._parseReply(reply) self._sendMissingMaterials(remote_materials_by_guid) - except json.JSONDecodeError as e: - Logger.log("e", "Error parsing materials from printer: %s", e) - except KeyError as e: - Logger.log("e", "Error parsing materials from printer: %s", e) + except json.JSONDecodeError: + Logger.logException("w", "Error parsing materials from printer") + except KeyError: + Logger.logException("w", "Error parsing materials from printer") ## Determine which materials should be updated and send them to the printer. # @@ -157,7 +157,6 @@ class SendMaterialJob(Job): @classmethod def _parseReply(cls, reply: QNetworkReply) -> Dict[str, ClusterMaterial]: remote_materials_list = json.loads(reply.readAll().data().decode("utf-8")) - print("remote_materials_list", remote_materials_list) return {material["guid"]: ClusterMaterial(**material) for material in remote_materials_list} ## Retrieves a list of local materials diff --git a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py index b51d978ed2..0e907f58eb 100644 --- a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py @@ -1,19 +1,13 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -import io -import json from typing import Any, List -from unittest import TestCase, mock +from unittest import TestCase from unittest.mock import patch, call from PyQt5.QtCore import QByteArray -from UM.Logger import Logger -from UM.MimeTypeDatabase import MimeType -from UM.Settings.ContainerRegistry import ContainerInterface, ContainerRegistryInterface, \ - DefinitionContainerInterface, ContainerRegistry +from UM.Settings.ContainerRegistry import ContainerInterface, ContainerRegistryInterface, DefinitionContainerInterface from plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice import ClusterUM3OutputDevice -from plugins.UM3NetworkPrinting.src.Models import ClusterMaterial from plugins.UM3NetworkPrinting.src.SendMaterialJob import SendMaterialJob @@ -45,7 +39,7 @@ class ContainerRegistryMock(ContainerRegistryInterface): return self.containersMetaData -class FakeDevice(ClusterUM3OutputDevice): +class MockOutputDevice(ClusterUM3OutputDevice): def _createFormPart(self, content_header, data, content_type=None): return "xxx" @@ -53,20 +47,20 @@ class FakeDevice(ClusterUM3OutputDevice): class TestSendMaterialJob(TestCase): _LOCAL_MATERIAL_WHITE = {'type': 'material', 'status': 'unknown', 'id': 'generic_pla_white', - 'base_file': 'generic_pla_white', 'setting_version': 5, 'name': 'White PLA', - 'brand': 'Generic', 'material': 'PLA', 'color_name': 'White', - 'GUID': 'badb0ee7-87c8-4f3f-9398-938587b67dce', 'version': '1', 'color_code': '#ffffff', - 'description': 'Test PLA White', 'adhesion_info': 'Use glue.', 'approximate_diameter': '3', - 'properties': {'density': '1.00', 'diameter': '2.85', 'weight': '750'}, - 'definition': 'fdmprinter', 'compatible': True} + 'base_file': 'generic_pla_white', 'setting_version': 5, 'name': 'White PLA', + 'brand': 'Generic', 'material': 'PLA', 'color_name': 'White', + 'GUID': 'badb0ee7-87c8-4f3f-9398-938587b67dce', 'version': '1', 'color_code': '#ffffff', + 'description': 'Test PLA White', 'adhesion_info': 'Use glue.', 'approximate_diameter': '3', + 'properties': {'density': '1.00', 'diameter': '2.85', 'weight': '750'}, + 'definition': 'fdmprinter', 'compatible': True} _LOCAL_MATERIAL_BLACK = {'type': 'material', 'status': 'unknown', 'id': 'generic_pla_black', - 'base_file': 'generic_pla_black', 'setting_version': 5, 'name': 'Yellow CPE', - 'brand': 'Ultimaker', 'material': 'CPE', 'color_name': 'Black', - 'GUID': '5fbb362a-41f9-4818-bb43-15ea6df34aa4', 'version': '1', 'color_code': '#000000', - 'description': 'Test PLA Black', 'adhesion_info': 'Use glue.', 'approximate_diameter': '3', - 'properties': {'density': '1.01', 'diameter': '2.85', 'weight': '750'}, - 'definition': 'fdmprinter', 'compatible': True} + 'base_file': 'generic_pla_black', 'setting_version': 5, 'name': 'Yellow CPE', + 'brand': 'Ultimaker', 'material': 'CPE', 'color_name': 'Black', + 'GUID': '5fbb362a-41f9-4818-bb43-15ea6df34aa4', 'version': '1', 'color_code': '#000000', + 'description': 'Test PLA Black', 'adhesion_info': 'Use glue.', 'approximate_diameter': '3', + 'properties': {'density': '1.01', 'diameter': '2.85', 'weight': '750'}, + 'definition': 'fdmprinter', 'compatible': True} _REMOTE_MATERIAL_WHITE = { "guid": "badb0ee7-87c8-4f3f-9398-938587b67dce", From d65114bd5652398bf9b3fae91dc38b53fae0e35f Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 19 Nov 2018 15:08:58 +0100 Subject: [PATCH 034/146] use call_count to assert device was not called --- .../tests/TestSendMaterialJob.py | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py index 0e907f58eb..9b1e1066ba 100644 --- a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py @@ -97,7 +97,7 @@ class TestSendMaterialJob(TestCase): # We expect the error string to be retrieved and the device not to be called for any follow up. self.assertEqual([call.attribute(0), call.errorString()], reply_mock.method_calls) - self.assertEqual([], device_mock.method_calls) + self.assertEqual(0, device_mock.call_count) @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") @patch("PyQt5.QtNetwork.QNetworkReply") @@ -110,7 +110,7 @@ class TestSendMaterialJob(TestCase): # We expect the reply to be called once to try to get the printers from the list (readAll()). # Given that the parsing there fails we do no expect the device to be called for any follow up. self.assertEqual([call.attribute(0), call.readAll()], reply_mock.method_calls) - self.assertEqual([], device_mock.method_calls) + self.assertEqual(0, device_mock.call_count) # @patch("PyQt5.QtNetwork.QNetworkReply") # def test_sendMissingMaterials_withMissingGuid(self, reply_mock): @@ -274,19 +274,3 @@ class TestSendMaterialJob(TestCase): # # self.assertTrue(len(local_materials) == 1) # self.assertTrue(list(local_materials.values())[0].version == 2) - # - # def _assertLogEntries(self, first, second): - # """ - # Inspects the two sets of log entry tuples and fails when they are not the same - # :param first: The first set of tuples - # :param second: The second set of tuples - # """ - # self.assertEqual(len(first), len(second)) - # - # while len(first) > 0: - # e1, m1 = first[0] - # e2, m2 = second[0] - # self.assertEqual(e1, e2) - # self.assertEqual(m1, m2) - # first.pop(0) - # second.pop(0) From 9d8583a3b67682442d0ab4383bdf748770105af2 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 19 Nov 2018 15:10:35 +0100 Subject: [PATCH 035/146] Revert "Fix running tests in plugin using pytest" This reverts commit f8f133d2ef92dfabb636eac70b09e681bd4b4d63. --- .../tests/TestLegacyProfileReader.py | 9 ++++----- .../tests/TestVersionUpgrade25to26.py | 11 +++-------- .../tests/TestVersionUpgrade26to27.py | 9 ++++----- .../tests/TestVersionUpgrade27to30.py | 8 ++++---- .../tests/TestVersionUpgrade34to35.py | 10 ++++------ 5 files changed, 19 insertions(+), 28 deletions(-) diff --git a/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py b/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py index 3c9e46b6d8..480a61f301 100644 --- a/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py +++ b/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py @@ -1,7 +1,8 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. + import configparser # An input for some functions we're testing. -import os # To find the integration test .ini files. +import os.path # To find the integration test .ini files. import pytest # To register tests with. import unittest.mock # To mock the application, plug-in and container registry out. @@ -10,15 +11,13 @@ import UM.PluginRegistry # To mock the plug-in registry out. import UM.Settings.ContainerRegistry # To mock the container registry out. import UM.Settings.InstanceContainer # To intercept the serialised data from the read() function. -import plugins.LegacyProfileReader.LegacyProfileReader as LegacyProfileReaderModule -from plugins.LegacyProfileReader.LegacyProfileReader import LegacyProfileReader - +import LegacyProfileReader as LegacyProfileReaderModule # To get the directory of the module. +from LegacyProfileReader import LegacyProfileReader # The module we're testing. @pytest.fixture def legacy_profile_reader(): return LegacyProfileReader() - test_prepareDefaultsData = [ { "defaults": diff --git a/plugins/VersionUpgrade/VersionUpgrade25to26/tests/TestVersionUpgrade25to26.py b/plugins/VersionUpgrade/VersionUpgrade25to26/tests/TestVersionUpgrade25to26.py index 588c0cb3db..9d7c7646cc 100644 --- a/plugins/VersionUpgrade/VersionUpgrade25to26/tests/TestVersionUpgrade25to26.py +++ b/plugins/VersionUpgrade/VersionUpgrade25to26/tests/TestVersionUpgrade25to26.py @@ -1,17 +1,16 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -import configparser -import pytest -from plugins.VersionUpgrade.VersionUpgrade25to26 import VersionUpgrade25to26 +import configparser #To check whether the appropriate exceptions are raised. +import pytest #To register tests with. +import VersionUpgrade25to26 #The module we're testing. ## Creates an instance of the upgrader to test with. @pytest.fixture def upgrader(): return VersionUpgrade25to26.VersionUpgrade25to26() - test_cfg_version_good_data = [ { "test_name": "Simple", @@ -61,7 +60,6 @@ setting_version = -3 } ] - ## Tests the technique that gets the version number from CFG files. # # \param data The parametrised data to test with. It contains a test name @@ -118,7 +116,6 @@ version = 1.2 } ] - ## Tests whether getting a version number from bad CFG files gives an # exception. # @@ -158,7 +155,6 @@ foo = bar } ] - ## Tests whether the settings that should be removed are removed for the 2.6 # version of preferences. @pytest.mark.parametrize("data", test_upgrade_preferences_removed_settings_data) @@ -204,7 +200,6 @@ type = instance_container } ] - ## Tests whether the settings that should be removed are removed for the 2.6 # version of instance containers. @pytest.mark.parametrize("data", test_upgrade_instance_container_removed_settings_data) diff --git a/plugins/VersionUpgrade/VersionUpgrade26to27/tests/TestVersionUpgrade26to27.py b/plugins/VersionUpgrade/VersionUpgrade26to27/tests/TestVersionUpgrade26to27.py index 45d41e7a1b..eebaca23c6 100644 --- a/plugins/VersionUpgrade/VersionUpgrade26to27/tests/TestVersionUpgrade26to27.py +++ b/plugins/VersionUpgrade/VersionUpgrade26to27/tests/TestVersionUpgrade26to27.py @@ -1,16 +1,15 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -import configparser -import pytest -from plugins.VersionUpgrade.VersionUpgrade26to27.VersionUpgrade26to27 import VersionUpgrade26to27 +import configparser #To check whether the appropriate exceptions are raised. +import pytest #To register tests with. +import VersionUpgrade26to27 #The module we're testing. ## Creates an instance of the upgrader to test with. @pytest.fixture def upgrader(): - return VersionUpgrade26to27() - + return VersionUpgrade26to27.VersionUpgrade26to27() test_cfg_version_good_data = [ { diff --git a/plugins/VersionUpgrade/VersionUpgrade27to30/tests/TestVersionUpgrade27to30.py b/plugins/VersionUpgrade/VersionUpgrade27to30/tests/TestVersionUpgrade27to30.py index 7b77b85993..cae08ebcfd 100644 --- a/plugins/VersionUpgrade/VersionUpgrade27to30/tests/TestVersionUpgrade27to30.py +++ b/plugins/VersionUpgrade/VersionUpgrade27to30/tests/TestVersionUpgrade27to30.py @@ -1,15 +1,15 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -import configparser -import pytest -from plugins.VersionUpgrade.VersionUpgrade27to30.VersionUpgrade27to30 import VersionUpgrade27to30 +import configparser #To parse the resulting config files. +import pytest #To register tests with. +import VersionUpgrade27to30 #The module we're testing. ## Creates an instance of the upgrader to test with. @pytest.fixture def upgrader(): - return VersionUpgrade27to30() + return VersionUpgrade27to30.VersionUpgrade27to30() test_cfg_version_good_data = [ { diff --git a/plugins/VersionUpgrade/VersionUpgrade34to35/tests/TestVersionUpgrade34to35.py b/plugins/VersionUpgrade/VersionUpgrade34to35/tests/TestVersionUpgrade34to35.py index 4f77fcd093..b74e6f35ac 100644 --- a/plugins/VersionUpgrade/VersionUpgrade34to35/tests/TestVersionUpgrade34to35.py +++ b/plugins/VersionUpgrade/VersionUpgrade34to35/tests/TestVersionUpgrade34to35.py @@ -1,16 +1,15 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -import configparser -import pytest -from plugins.VersionUpgrade.VersionUpgrade34to35.VersionUpgrade34to35 import VersionUpgrade34to35 +import configparser #To parse the resulting config files. +import pytest #To register tests with. +import VersionUpgrade34to35 #The module we're testing. ## Creates an instance of the upgrader to test with. @pytest.fixture def upgrader(): - return VersionUpgrade34to35() - + return VersionUpgrade34to35.VersionUpgrade34to35() test_upgrade_version_nr_data = [ ("Empty config file", @@ -26,7 +25,6 @@ test_upgrade_version_nr_data = [ ) ] - ## Tests whether the version numbers are updated. @pytest.mark.parametrize("test_name, file_data", test_upgrade_version_nr_data) def test_upgradeVersionNr(test_name, file_data, upgrader): From 66fbadf2dea2a734a4055b8f866b5397303b2069 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 19 Nov 2018 15:37:56 +0100 Subject: [PATCH 036/146] Convert all single quotes to double quotes --- .../tests/TestSendMaterialJob.py | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py index 9b1e1066ba..22e96f5ed0 100644 --- a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py @@ -46,21 +46,21 @@ class MockOutputDevice(ClusterUM3OutputDevice): class TestSendMaterialJob(TestCase): - _LOCAL_MATERIAL_WHITE = {'type': 'material', 'status': 'unknown', 'id': 'generic_pla_white', - 'base_file': 'generic_pla_white', 'setting_version': 5, 'name': 'White PLA', - 'brand': 'Generic', 'material': 'PLA', 'color_name': 'White', - 'GUID': 'badb0ee7-87c8-4f3f-9398-938587b67dce', 'version': '1', 'color_code': '#ffffff', - 'description': 'Test PLA White', 'adhesion_info': 'Use glue.', 'approximate_diameter': '3', - 'properties': {'density': '1.00', 'diameter': '2.85', 'weight': '750'}, - 'definition': 'fdmprinter', 'compatible': True} + _LOCAL_MATERIAL_WHITE = {"type": "material", "status": "unknown", "id": "generic_pla_white", + "base_file": "generic_pla_white", "setting_version": 5, "name": "White PLA", + "brand": "Generic", "material": "PLA", "color_name": "White", + "GUID": "badb0ee7-87c8-4f3f-9398-938587b67dce", "version": "1", "color_code": "#ffffff", + "description": "Test PLA White", "adhesion_info": "Use glue.", "approximate_diameter": "3", + "properties": {"density": "1.00", "diameter": "2.85", "weight": "750"}, + "definition": "fdmprinter", "compatible": True} - _LOCAL_MATERIAL_BLACK = {'type': 'material', 'status': 'unknown', 'id': 'generic_pla_black', - 'base_file': 'generic_pla_black', 'setting_version': 5, 'name': 'Yellow CPE', - 'brand': 'Ultimaker', 'material': 'CPE', 'color_name': 'Black', - 'GUID': '5fbb362a-41f9-4818-bb43-15ea6df34aa4', 'version': '1', 'color_code': '#000000', - 'description': 'Test PLA Black', 'adhesion_info': 'Use glue.', 'approximate_diameter': '3', - 'properties': {'density': '1.01', 'diameter': '2.85', 'weight': '750'}, - 'definition': 'fdmprinter', 'compatible': True} + _LOCAL_MATERIAL_BLACK = {"type": "material", "status": "unknown", "id": "generic_pla_black", + "base_file": "generic_pla_black", "setting_version": 5, "name": "Yellow CPE", + "brand": "Ultimaker", "material": "CPE", "color_name": "Black", + "GUID": "5fbb362a-41f9-4818-bb43-15ea6df34aa4", "version": "1", "color_code": "#000000", + "description": "Test PLA Black", "adhesion_info": "Use glue.", "approximate_diameter": "3", + "properties": {"density": "1.01", "diameter": "2.85", "weight": "750"}, + "definition": "fdmprinter", "compatible": True} _REMOTE_MATERIAL_WHITE = { "guid": "badb0ee7-87c8-4f3f-9398-938587b67dce", @@ -103,7 +103,7 @@ class TestSendMaterialJob(TestCase): @patch("PyQt5.QtNetwork.QNetworkReply") def test_sendMissingMaterials_withBadJsonAnswer(self, reply_mock, device_mock): reply_mock.attribute.return_value = 200 - reply_mock.readAll.return_value = QByteArray(b'Six sick hicks nick six slick bricks with picks and sticks.') + reply_mock.readAll.return_value = QByteArray(b"Six sick hicks nick six slick bricks with picks and sticks.") job = SendMaterialJob(device_mock) job._onGetRemoteMaterials(reply_mock) @@ -119,13 +119,13 @@ class TestSendMaterialJob(TestCase): # del remoteMaterialWithoutGuid["guid"] # reply_mock.readAll.return_value = QByteArray(json.dumps([remoteMaterialWithoutGuid]).encode("ascii")) # - # with mock.patch.object(Logger, 'log', new=new_log): + # with mock.patch.object(Logger, "log", new=new_log): # SendMaterialJob(None).sendMissingMaterials(reply_mock) # # reply_mock.attribute.assert_called_with(0) # self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) # self._assertLogEntries( - # [('e', "Request material storage on printer: Printer's answer was missing GUIDs.")], + # [("e", "Request material storage on printer: Printer"s answer was missing GUIDs.")], # _logentries) # # @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: []) @@ -145,7 +145,7 @@ class TestSendMaterialJob(TestCase): # # reply_mock.attribute.assert_called_with(0) # self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) - # self._assertLogEntries([('e', "Material generic_pla_white has invalid version number one.")], _logentries) + # self._assertLogEntries([("e", "Material generic_pla_white has invalid version number one.")], _logentries) # # @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: []) # @patch("PyQt5.QtNetwork.QNetworkReply") @@ -194,16 +194,16 @@ class TestSendMaterialJob(TestCase): # device_mock._createFormPart.return_value = "_xXx_" # with mock.patch.object(Logger, "log", new=new_log): # job = SendMaterialJob(device_mock) - # job.sendMaterialsToPrinter({'generic_pla_white'}) + # job.sendMaterialsToPrinter({"generic_pla_white"}) # # self._assertLogEntries([("d", "Syncing material generic_pla_white with cluster.")], _logentries) - # self.assertEqual([call._createFormPart('name="file"; filename="generic_pla_white.xml.fdm_material"', ''), + # self.assertEqual([call._createFormPart("name="file"; filename="generic_pla_white.xml.fdm_material"", ""), # call.postFormWithParts(on_finished=job.sendingFinished, parts = ["_xXx_"], target = "materials/")], device_mock.method_calls) # # @patch("PyQt5.QtNetwork.QNetworkReply") # def test_sendingFinished_success(self, reply_mock) -> None: # reply_mock.attribute.return_value = 200 - # with mock.patch.object(Logger, 'log', new=new_log): + # with mock.patch.object(Logger, "log", new=new_log): # SendMaterialJob(None).sendingFinished(reply_mock) # # reply_mock.attribute.assert_called_once_with(0) @@ -212,9 +212,9 @@ class TestSendMaterialJob(TestCase): # @patch("PyQt5.QtNetwork.QNetworkReply") # def test_sendingFinished_failed(self, reply_mock) -> None: # reply_mock.attribute.return_value = 404 - # reply_mock.readAll.return_value = QByteArray(b'Six sick hicks nick six slick bricks with picks and sticks.') + # reply_mock.readAll.return_value = QByteArray(b"Six sick hicks nick six slick bricks with picks and sticks.") # - # with mock.patch.object(Logger, 'log', new=new_log): + # with mock.patch.object(Logger, "log", new=new_log): # SendMaterialJob(None).sendingFinished(reply_mock) # # reply_mock.attribute.assert_called_with(0) From 60dd1303936a09c83a562067c9a8193c2f3d5e43 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 19 Nov 2018 15:39:12 +0100 Subject: [PATCH 037/146] Use logException where possible --- plugins/UM3NetworkPrinting/src/SendMaterialJob.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py index 62414763b6..6260752f3f 100644 --- a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py @@ -175,10 +175,7 @@ class SendMaterialJob(Job): material = LocalMaterial(**m) if material.GUID not in result or material.version > result.get(material.GUID).version: result[material.GUID] = material - except ValueError as e: - Logger.log("w", "Local material {material_id} has invalid values: {e}".format( - material_id = m["id"], - e = e - )) + except ValueError: + Logger.logException("w", "Local material {} has invalid values.".format(m["id"])) return result From c04ce7fce8df24298c1708a044ebfa4afee8a411 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 19 Nov 2018 15:44:07 +0100 Subject: [PATCH 038/146] Use call_count on specific method to be more precise --- plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py index 22e96f5ed0..a71ded75b6 100644 --- a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py @@ -97,7 +97,7 @@ class TestSendMaterialJob(TestCase): # We expect the error string to be retrieved and the device not to be called for any follow up. self.assertEqual([call.attribute(0), call.errorString()], reply_mock.method_calls) - self.assertEqual(0, device_mock.call_count) + self.assertEqual(0, device_mock.createFormPart.call_count) @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") @patch("PyQt5.QtNetwork.QNetworkReply") @@ -110,7 +110,7 @@ class TestSendMaterialJob(TestCase): # We expect the reply to be called once to try to get the printers from the list (readAll()). # Given that the parsing there fails we do no expect the device to be called for any follow up. self.assertEqual([call.attribute(0), call.readAll()], reply_mock.method_calls) - self.assertEqual(0, device_mock.call_count) + self.assertEqual(0, device_mock.createFormPart.call_count) # @patch("PyQt5.QtNetwork.QNetworkReply") # def test_sendMissingMaterials_withMissingGuid(self, reply_mock): From 2497325d606a9246bb383fa87e2dc1707f99e321 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 19 Nov 2018 16:35:19 +0100 Subject: [PATCH 039/146] Test with named tuples, not working yet --- plugins/UM3NetworkPrinting/src/Models.py | 111 +++++------------- .../UM3NetworkPrinting/src/SendMaterialJob.py | 14 +-- .../tests/TestSendMaterialJob.py | 35 +++--- 3 files changed, 53 insertions(+), 107 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Models.py b/plugins/UM3NetworkPrinting/src/Models.py index 89bf665377..e8efa577f6 100644 --- a/plugins/UM3NetworkPrinting/src/Models.py +++ b/plugins/UM3NetworkPrinting/src/Models.py @@ -1,87 +1,32 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Optional +from collections import namedtuple +ClusterMaterial = namedtuple('ClusterMaterial', [ + 'guid', + 'material', + 'brand', + 'version', + 'color', + 'density' +]) -class BaseModel: - def __init__(self, **kwargs): - self.__dict__.update(kwargs) - - def __eq__(self, other): - return self.__dict__ == other.__dict__ if type(self) == type(other) else False - - -## Represents an item in the cluster API response for installed materials. -class ClusterMaterial(BaseModel): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.version = int(self.version) - self.density = float(self.density) - - guid = None # type: Optional[str] - - material = None # type: Optional[str] - - brand = None # type: Optional[str] - - version = None # type: Optional[int] - - color = None # type: Optional[str] - - density = None # type: Optional[float] - - -class LocalMaterialProperties(BaseModel): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.density = float(self.density) - self.diameter = float(self.diameter) - self.weight = float(self.weight) - - density = None # type: Optional[float] - - diameter = None # type: Optional[float] - - weight = None # type: Optional[int] - - -class LocalMaterial(BaseModel): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.properties = LocalMaterialProperties(**self.properties) - self.approximate_diameter = float(self.approximate_diameter) - self.version = int(self.version) - - GUID = None # type: Optional[str] - - id = None # type: Optional[str] - - type = None # type: Optional[str] - - status = None # type: Optional[str] - - base_file = None # type: Optional[str] - - setting_version = None # type: Optional[str] - - version = None # type: Optional[int] - - name = None # type: Optional[str] - - brand = None # type: Optional[str] - - material = None # type: Optional[str] - - color_name = None # type: Optional[str] - - description = None # type: Optional[str] - - adhesion_info = None # type: Optional[str] - - approximate_diameter = None # type: Optional[float] - - properties = None # type: LocalMaterialProperties - - definition = None # type: Optional[str] - - compatible = None # type: Optional[bool] +LocalMaterial = namedtuple('LocalMaterial', [ + 'GUID', + 'id', + 'type', + 'status', + 'base_file', + 'setting_version', + 'version', + 'name', + 'brand', + 'material', + 'color_name', + 'description', + 'adhesion_info', + 'approximate_diameter', + 'properties', + 'definition', + 'compatible' +]) diff --git a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py index 6260752f3f..cbe79aef6a 100644 --- a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py @@ -156,8 +156,8 @@ class SendMaterialJob(Job): # \throw KeyError Raised when on of the materials does not include a valid guid @classmethod def _parseReply(cls, reply: QNetworkReply) -> Dict[str, ClusterMaterial]: - remote_materials_list = json.loads(reply.readAll().data().decode("utf-8")) - return {material["guid"]: ClusterMaterial(**material) for material in remote_materials_list} + remote_materials = json.loads(reply.readAll().data().decode("utf-8")) + return {material["id"]: ClusterMaterial(**material) for material in remote_materials} ## Retrieves a list of local materials # @@ -170,12 +170,12 @@ class SendMaterialJob(Job): material_containers = container_registry.findContainersMetadata(type = "material") # Find the latest version of all material containers in the registry. - for m in material_containers: + local_materials = {} # type: Dict[str, LocalMaterial] + for material in material_containers: try: - material = LocalMaterial(**m) + material = LocalMaterial(**material) if material.GUID not in result or material.version > result.get(material.GUID).version: - result[material.GUID] = material + local_materials[material.GUID] = material except ValueError: - Logger.logException("w", "Local material {} has invalid values.".format(m["id"])) - + Logger.logException("w", "Local material {} has invalid values.".format(material["id"])) return result diff --git a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py index a71ded75b6..73bca2b0ad 100644 --- a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py @@ -1,5 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import json + from typing import Any, List from unittest import TestCase from unittest.mock import patch, call @@ -108,26 +110,25 @@ class TestSendMaterialJob(TestCase): job._onGetRemoteMaterials(reply_mock) # We expect the reply to be called once to try to get the printers from the list (readAll()). - # Given that the parsing there fails we do no expect the device to be called for any follow up. + # Given that the parsing fails we do no expect the device to be called for any follow up. self.assertEqual([call.attribute(0), call.readAll()], reply_mock.method_calls) self.assertEqual(0, device_mock.createFormPart.call_count) - # @patch("PyQt5.QtNetwork.QNetworkReply") - # def test_sendMissingMaterials_withMissingGuid(self, reply_mock): - # reply_mock.attribute.return_value = 200 - # remoteMaterialWithoutGuid = self._REMOTEMATERIAL_WHITE.copy() - # del remoteMaterialWithoutGuid["guid"] - # reply_mock.readAll.return_value = QByteArray(json.dumps([remoteMaterialWithoutGuid]).encode("ascii")) - # - # with mock.patch.object(Logger, "log", new=new_log): - # SendMaterialJob(None).sendMissingMaterials(reply_mock) - # - # reply_mock.attribute.assert_called_with(0) - # self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) - # self._assertLogEntries( - # [("e", "Request material storage on printer: Printer"s answer was missing GUIDs.")], - # _logentries) - # + @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") + @patch("PyQt5.QtNetwork.QNetworkReply") + def test_sendMissingMaterials_withMissingGuid(self, reply_mock, device_mock): + reply_mock.attribute.return_value = 200 + remote_material_without_guid = self._REMOTE_MATERIAL_WHITE.copy() + del remote_material_without_guid["guid"] + reply_mock.readAll.return_value = QByteArray(json.dumps([remote_material_without_guid]).encode("ascii")) + job = SendMaterialJob(device_mock) + job._onGetRemoteMaterials(reply_mock) + + # We expect the reply to be called once to try to get the printers from the list (readAll()). + # Given that parsing fails we do not expect the device to be called for any follow up. + self.assertEqual([call.attribute(0), call.readAll()], reply_mock.method_calls) + self.assertEqual(1, device_mock.createFormPart.call_count) + # @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: []) # @patch("PyQt5.QtNetwork.QNetworkReply") # def test_sendMissingMaterials_WithInvalidVersionInLocalMaterial(self, reply_mock): From 481ca8cd2f2e61ba7591695c7b318346cad4364d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijn=20De=C3=A9?= Date: Tue, 20 Nov 2018 16:33:52 +0100 Subject: [PATCH 040/146] Fixed some bugs and added the color_code field to the named tuple --- plugins/UM3NetworkPrinting/src/Models.py | 1 + .../UM3NetworkPrinting/src/SendMaterialJob.py | 20 +++++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Models.py b/plugins/UM3NetworkPrinting/src/Models.py index e8efa577f6..a9210ac5b4 100644 --- a/plugins/UM3NetworkPrinting/src/Models.py +++ b/plugins/UM3NetworkPrinting/src/Models.py @@ -23,6 +23,7 @@ LocalMaterial = namedtuple('LocalMaterial', [ 'brand', 'material', 'color_name', + 'color_code', 'description', 'adhesion_info', 'approximate_diameter', diff --git a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py index cbe79aef6a..0599101379 100644 --- a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py @@ -2,6 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. import json import os +import re import urllib.parse from typing import Dict, TYPE_CHECKING, Set @@ -19,7 +20,6 @@ from .Models import ClusterMaterial, LocalMaterial if TYPE_CHECKING: from .ClusterUM3OutputDevice import ClusterUM3OutputDevice - ## Asynchronous job to send material profiles to the printer. # # This way it won't freeze up the interface while sending those materials. @@ -50,7 +50,7 @@ class SendMaterialJob(Job): self._sendMissingMaterials(remote_materials_by_guid) except json.JSONDecodeError: Logger.logException("w", "Error parsing materials from printer") - except KeyError: + except TypeError: Logger.logException("w", "Error parsing materials from printer") ## Determine which materials should be updated and send them to the printer. @@ -75,7 +75,8 @@ class SendMaterialJob(Job): ## From the local and remote materials, determine which ones should be synchronized. # - # Makes a Set containing only the materials that are not on the printer yet or the ones that are newer in Cura. + # Makes a Set of id's containing only the id's of the materials that are not on the printer yet or the ones that + # are newer in Cura. # # \param local_materials The local materials by GUID. # \param remote_materials The remote materials by GUID. @@ -157,7 +158,7 @@ class SendMaterialJob(Job): @classmethod def _parseReply(cls, reply: QNetworkReply) -> Dict[str, ClusterMaterial]: remote_materials = json.loads(reply.readAll().data().decode("utf-8")) - return {material["id"]: ClusterMaterial(**material) for material in remote_materials} + return {material["guid"]: ClusterMaterial(**material) for material in remote_materials} ## Retrieves a list of local materials # @@ -170,12 +171,19 @@ class SendMaterialJob(Job): material_containers = container_registry.findContainersMetadata(type = "material") # Find the latest version of all material containers in the registry. - local_materials = {} # type: Dict[str, LocalMaterial] for material in material_containers: try: material = LocalMaterial(**material) + + # material version must be an int + if not re.match("\d+", material.version): + Logger.logException("w", "Local material {} has invalid version '{}'." + .format(material["id"], material.version)) + continue + if material.GUID not in result or material.version > result.get(material.GUID).version: - local_materials[material.GUID] = material + result[material.GUID] = material except ValueError: Logger.logException("w", "Local material {} has invalid values.".format(material["id"])) + return result From ca6074429290b6ae5fa1f3ccfbfa74107ed1284a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijn=20De=C3=A9?= Date: Tue, 20 Nov 2018 16:34:11 +0100 Subject: [PATCH 041/146] Made the tests work with the named tuples Tests only use the _onGetRemoteMaterial --- .../tests/TestSendMaterialJob.py | 313 +++++++----------- 1 file changed, 123 insertions(+), 190 deletions(-) diff --git a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py index 73bca2b0ad..f4604580fe 100644 --- a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py @@ -1,53 +1,23 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import io import json - -from typing import Any, List -from unittest import TestCase +from unittest import TestCase, mock from unittest.mock import patch, call from PyQt5.QtCore import QByteArray -from UM.Settings.ContainerRegistry import ContainerInterface, ContainerRegistryInterface, DefinitionContainerInterface -from plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice import ClusterUM3OutputDevice +from UM.MimeTypeDatabase import MimeType +from cura.CuraApplication import CuraApplication from plugins.UM3NetworkPrinting.src.SendMaterialJob import SendMaterialJob -class ContainerRegistryMock(ContainerRegistryInterface): - - def __init__(self): - self.containersMetaData = None - - def findContainers(self, *, ignore_case: bool = False, **kwargs: Any) -> List[ContainerInterface]: - raise NotImplementedError() - - def findDefinitionContainers(self, **kwargs: Any) -> List[DefinitionContainerInterface]: - raise NotImplementedError() - - @classmethod - def getApplication(cls) -> "Application": - raise NotImplementedError() - - def getEmptyInstanceContainer(self) -> "InstanceContainer": - raise NotImplementedError() - - def isReadOnly(self, container_id: str) -> bool: - raise NotImplementedError() - - def setContainersMetadata(self, value): - self.containersMetaData = value - - def findContainersMetadata(self, type): - return self.containersMetaData - - -class MockOutputDevice(ClusterUM3OutputDevice): - def _createFormPart(self, content_header, data, content_type=None): - return "xxx" - - +@patch("builtins.open", lambda _, __: io.StringIO("")) +@patch("UM.MimeTypeDatabase.MimeTypeDatabase.getMimeTypeForFile", + lambda _: MimeType(name = "application/x-ultimaker-material-profile", comment = "Ultimaker Material Profile", + suffixes = ["xml.fdm_material"])) +@patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: ["/materials/generic_pla_white.xml.fdm_material"]) class TestSendMaterialJob(TestCase): - _LOCAL_MATERIAL_WHITE = {"type": "material", "status": "unknown", "id": "generic_pla_white", "base_file": "generic_pla_white", "setting_version": 5, "name": "White PLA", "brand": "Generic", "material": "PLA", "color_name": "White", @@ -88,11 +58,11 @@ class TestSendMaterialJob(TestCase): job.run() # We expect the materials endpoint to be called when the job runs. - device_mock.get.assert_called_with("materials/", on_finished=job._onGetRemoteMaterials) + device_mock.get.assert_called_with("materials/", on_finished = job._onGetRemoteMaterials) @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") @patch("PyQt5.QtNetwork.QNetworkReply") - def test_sendMissingMaterials_withFailedRequest(self, reply_mock, device_mock): + def test__onGetRemoteMaterials_withFailedRequest(self, reply_mock, device_mock): reply_mock.attribute.return_value = 404 job = SendMaterialJob(device_mock) job._onGetRemoteMaterials(reply_mock) @@ -103,7 +73,7 @@ class TestSendMaterialJob(TestCase): @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") @patch("PyQt5.QtNetwork.QNetworkReply") - def test_sendMissingMaterials_withBadJsonAnswer(self, reply_mock, device_mock): + def test__onGetRemoteMaterials_withBadJsonAnswer(self, reply_mock, device_mock): reply_mock.attribute.return_value = 200 reply_mock.readAll.return_value = QByteArray(b"Six sick hicks nick six slick bricks with picks and sticks.") job = SendMaterialJob(device_mock) @@ -116,7 +86,7 @@ class TestSendMaterialJob(TestCase): @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") @patch("PyQt5.QtNetwork.QNetworkReply") - def test_sendMissingMaterials_withMissingGuid(self, reply_mock, device_mock): + def test__onGetRemoteMaterials_withMissingGuidInRemoteMaterial(self, reply_mock, device_mock): reply_mock.attribute.return_value = 200 remote_material_without_guid = self._REMOTE_MATERIAL_WHITE.copy() del remote_material_without_guid["guid"] @@ -127,151 +97,114 @@ class TestSendMaterialJob(TestCase): # We expect the reply to be called once to try to get the printers from the list (readAll()). # Given that parsing fails we do not expect the device to be called for any follow up. self.assertEqual([call.attribute(0), call.readAll()], reply_mock.method_calls) - self.assertEqual(1, device_mock.createFormPart.call_count) + self.assertEqual(0, device_mock.createFormPart.call_count) - # @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: []) - # @patch("PyQt5.QtNetwork.QNetworkReply") - # def test_sendMissingMaterials_WithInvalidVersionInLocalMaterial(self, reply_mock): - # reply_mock.attribute.return_value = 200 - # reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTEMATERIAL_WHITE]).encode("ascii")) - # - # containerRegistry = ContainerRegistryMock() - # localMaterialWhiteWithInvalidVersion = self._LOCALMATERIAL_WHITE.copy() - # localMaterialWhiteWithInvalidVersion["version"] = "one" - # containerRegistry.setContainersMetadata([localMaterialWhiteWithInvalidVersion]) - # - # with mock.patch.object(Logger, "log", new=new_log): - # with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): - # SendMaterialJob(None).sendMissingMaterials(reply_mock) - # - # reply_mock.attribute.assert_called_with(0) - # self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) - # self._assertLogEntries([("e", "Material generic_pla_white has invalid version number one.")], _logentries) - # - # @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: []) - # @patch("PyQt5.QtNetwork.QNetworkReply") - # def test_sendMissingMaterials_WithMultipleLocalVersionsLowFirst(self, reply_mock): - # reply_mock.attribute.return_value = 200 - # reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTEMATERIAL_WHITE]).encode("ascii")) - # - # containerRegistry = ContainerRegistryMock() - # localMaterialWhiteWithHigherVersion = self._LOCALMATERIAL_WHITE.copy() - # localMaterialWhiteWithHigherVersion["version"] = "2" - # containerRegistry.setContainersMetadata([self._LOCALMATERIAL_WHITE, localMaterialWhiteWithHigherVersion]) - # - # with mock.patch.object(Logger, "log", new=new_log): - # with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): - # SendMaterialJob(None).sendMissingMaterials(reply_mock) - # - # reply_mock.attribute.assert_called_with(0) - # self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) - # self._assertLogEntries([], _logentries) - # - # @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: []) - # @patch("PyQt5.QtNetwork.QNetworkReply") - # def test_sendMissingMaterials_MaterialMissingOnPrinter(self, reply_mock): - # reply_mock.attribute.return_value = 200 - # reply_mock.readAll.return_value = QByteArray( - # json.dumps([self._REMOTEMATERIAL_WHITE]).encode("ascii")) - # - # containerRegistry = ContainerRegistryMock() - # containerRegistry.setContainersMetadata([self._LOCALMATERIAL_WHITE, self._LOCALMATERIAL_BLACK]) - # - # with mock.patch.object(Logger, "log", new=new_log): - # with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): - # SendMaterialJob(None).sendMissingMaterials(reply_mock) - # - # reply_mock.attribute.assert_called_with(0) - # self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.readAll()]) - # self._assertLogEntries([], _logentries) - # - # @patch("builtins.open", lambda a, b: io.StringIO("")) - # @patch("UM.MimeTypeDatabase.MimeTypeDatabase.getMimeTypeForFile", - # lambda _: MimeType(name="application/x-ultimaker-material-profile", comment="Ultimaker Material Profile", - # suffixes=["xml.fdm_material"])) - # @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: ["/materials/generic_pla_white.xml.fdm_material"]) - # @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") - # def test_sendMaterialsToPrinter(self, device_mock): - # device_mock._createFormPart.return_value = "_xXx_" - # with mock.patch.object(Logger, "log", new=new_log): - # job = SendMaterialJob(device_mock) - # job.sendMaterialsToPrinter({"generic_pla_white"}) - # - # self._assertLogEntries([("d", "Syncing material generic_pla_white with cluster.")], _logentries) - # self.assertEqual([call._createFormPart("name="file"; filename="generic_pla_white.xml.fdm_material"", ""), - # call.postFormWithParts(on_finished=job.sendingFinished, parts = ["_xXx_"], target = "materials/")], device_mock.method_calls) - # - # @patch("PyQt5.QtNetwork.QNetworkReply") - # def test_sendingFinished_success(self, reply_mock) -> None: - # reply_mock.attribute.return_value = 200 - # with mock.patch.object(Logger, "log", new=new_log): - # SendMaterialJob(None).sendingFinished(reply_mock) - # - # reply_mock.attribute.assert_called_once_with(0) - # self.assertEqual(0, len(_logentries)) - # - # @patch("PyQt5.QtNetwork.QNetworkReply") - # def test_sendingFinished_failed(self, reply_mock) -> None: - # reply_mock.attribute.return_value = 404 - # reply_mock.readAll.return_value = QByteArray(b"Six sick hicks nick six slick bricks with picks and sticks.") - # - # with mock.patch.object(Logger, "log", new=new_log): - # SendMaterialJob(None).sendingFinished(reply_mock) - # - # reply_mock.attribute.assert_called_with(0) - # self.assertEqual(reply_mock.method_calls, [call.attribute(0), call.attribute(0), call.readAll()]) - # - # self._assertLogEntries([ - # ("e", "Received error code from printer when syncing material: 404"), - # ("e", "Six sick hicks nick six slick bricks with picks and sticks.") - # ], _logentries) - # - # @patch("PyQt5.QtNetwork.QNetworkReply") - # def test_parseReply(self, reply_mock): - # reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTEMATERIAL_WHITE]).encode("ascii")) - # - # response = SendMaterialJob._parseReply(reply_mock) - # - # self.assertTrue(len(response) == 1) - # self.assertEqual(next(iter(response.values())), ClusterMaterial(**self._REMOTEMATERIAL_WHITE)) - # - # @patch("PyQt5.QtNetwork.QNetworkReply") - # def test_parseReplyWithInvalidMaterial(self, reply_mock): - # remoteMaterialWithInvalidVersion = self._REMOTEMATERIAL_WHITE.copy() - # remoteMaterialWithInvalidVersion["version"] = "one" - # reply_mock.readAll.return_value = QByteArray(json.dumps([remoteMaterialWithInvalidVersion]).encode("ascii")) - # - # with self.assertRaises(ValueError): - # SendMaterialJob._parseReply(reply_mock) - # - # def test__getLocalMaterials(self): - # containerRegistry = ContainerRegistryMock() - # containerRegistry.setContainersMetadata([self._LOCALMATERIAL_WHITE, self._LOCALMATERIAL_BLACK]) - # - # with mock.patch.object(Logger, "log", new=new_log): - # with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): - # local_materials = SendMaterialJob(None)._getLocalMaterials() - # - # self.assertTrue(len(local_materials) == 2) - # - # def test__getLocalMaterialsWithMultipleVersions(self): - # containerRegistry = ContainerRegistryMock() - # localMaterialWithNewerVersion = self._LOCALMATERIAL_WHITE.copy() - # localMaterialWithNewerVersion["version"] = 2 - # containerRegistry.setContainersMetadata([self._LOCALMATERIAL_WHITE, localMaterialWithNewerVersion]) - # - # with mock.patch.object(Logger, "log", new=new_log): - # with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): - # local_materials = SendMaterialJob(None)._getLocalMaterials() - # - # self.assertTrue(len(local_materials) == 1) - # self.assertTrue(list(local_materials.values())[0].version == 2) - # - # containerRegistry.setContainersMetadata([localMaterialWithNewerVersion, self._LOCALMATERIAL_WHITE]) - # - # with mock.patch.object(Logger, "log", new=new_log): - # with mock.patch.object(ContainerRegistry, "getInstance", lambda: containerRegistry): - # local_materials = SendMaterialJob(None)._getLocalMaterials() - # - # self.assertTrue(len(local_materials) == 1) - # self.assertTrue(list(local_materials.values())[0].version == 2) + @patch("cura.Settings.CuraContainerRegistry") + @patch("cura.CuraApplication") + @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") + @patch("PyQt5.QtNetwork.QNetworkReply") + def test__onGetRemoteMaterials_withInvalidVersionInLocalMaterial(self, reply_mock, device_mock, application_mock, + container_registry_mock): + reply_mock.attribute.return_value = 200 + reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii")) + + localMaterialWhiteWithInvalidVersion = self._LOCAL_MATERIAL_WHITE.copy() + localMaterialWhiteWithInvalidVersion["version"] = "one" + container_registry_mock.findContainersMetadata.return_value = [localMaterialWhiteWithInvalidVersion] + + application_mock.getContainerRegistry.return_value = container_registry_mock + + with mock.patch.object(CuraApplication, "getInstance", new = lambda: application_mock): + job = SendMaterialJob(device_mock) + job._onGetRemoteMaterials(reply_mock) + + self.assertEqual([call.attribute(0), call.readAll()], reply_mock.method_calls) + self.assertEqual([call.getContainerRegistry()], application_mock.method_calls) + self.assertEqual([call.findContainersMetadata(type = "material")], container_registry_mock.method_calls) + self.assertEqual(0, device_mock.createFormPart.call_count) + + @patch("cura.Settings.CuraContainerRegistry") + @patch("cura.CuraApplication") + @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") + @patch("PyQt5.QtNetwork.QNetworkReply") + def test__onGetRemoteMaterials_withNoUpdate(self, reply_mock, device_mock, application_mock, + container_registry_mock): + application_mock.getContainerRegistry.return_value = container_registry_mock + + device_mock.createFormPart.return_value = "_xXx_" + + container_registry_mock.findContainersMetadata.return_value = [self._LOCAL_MATERIAL_WHITE] + + reply_mock.attribute.return_value = 200 + reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii")) + + with mock.patch.object(CuraApplication, "getInstance", new = lambda: application_mock): + job = SendMaterialJob(device_mock) + job._onGetRemoteMaterials(reply_mock) + + self.assertEqual([call.attribute(0), call.readAll()], reply_mock.method_calls) + self.assertEqual([call.getContainerRegistry()], application_mock.method_calls) + self.assertEqual([call.findContainersMetadata(type = "material")], container_registry_mock.method_calls) + self.assertEqual(0, device_mock.createFormPart.call_count) + self.assertEqual(0, device_mock.postFormWithParts.call_count) + + @patch("cura.Settings.CuraContainerRegistry") + @patch("cura.CuraApplication") + @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") + @patch("PyQt5.QtNetwork.QNetworkReply") + def test__onGetRemoteMaterials_withUpdatedMaterial(self, reply_mock, device_mock, application_mock, + container_registry_mock): + application_mock.getContainerRegistry.return_value = container_registry_mock + + device_mock.createFormPart.return_value = "_xXx_" + + localMaterialWhiteWithHigherVersion = self._LOCAL_MATERIAL_WHITE.copy() + localMaterialWhiteWithHigherVersion["version"] = "2" + container_registry_mock.findContainersMetadata.return_value = [localMaterialWhiteWithHigherVersion] + + reply_mock.attribute.return_value = 200 + reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii")) + + with mock.patch.object(CuraApplication, "getInstance", new = lambda: application_mock): + job = SendMaterialJob(device_mock) + job._onGetRemoteMaterials(reply_mock) + + self.assertEqual([call.attribute(0), call.readAll()], reply_mock.method_calls) + self.assertEqual([call.getContainerRegistry()], application_mock.method_calls) + self.assertEqual([call.findContainersMetadata(type = "material")], container_registry_mock.method_calls) + self.assertEqual(1, device_mock.createFormPart.call_count) + self.assertEqual(1, device_mock.postFormWithParts.call_count) + self.assertEquals( + [call.createFormPart("name=\"file\"; filename=\"generic_pla_white.xml.fdm_material\"", ""), + call.postFormWithParts(target = "materials/", parts = ["_xXx_"], on_finished = job.sendingFinished)], + device_mock.method_calls) + + @patch("cura.Settings.CuraContainerRegistry") + @patch("cura.CuraApplication") + @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") + @patch("PyQt5.QtNetwork.QNetworkReply") + def test__onGetRemoteMaterials_withNewMaterial(self, reply_mock, device_mock, application_mock, + container_registry_mock): + application_mock.getContainerRegistry.return_value = container_registry_mock + + device_mock.createFormPart.return_value = "_xXx_" + + container_registry_mock.findContainersMetadata.return_value = [self._LOCAL_MATERIAL_WHITE, + self._LOCAL_MATERIAL_BLACK] + + reply_mock.attribute.return_value = 200 + reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_BLACK]).encode("ascii")) + + with mock.patch.object(CuraApplication, "getInstance", new = lambda: application_mock): + job = SendMaterialJob(device_mock) + job._onGetRemoteMaterials(reply_mock) + + self.assertEqual([call.attribute(0), call.readAll()], reply_mock.method_calls) + self.assertEqual([call.getContainerRegistry()], application_mock.method_calls) + self.assertEqual([call.findContainersMetadata(type = "material")], container_registry_mock.method_calls) + self.assertEqual(1, device_mock.createFormPart.call_count) + self.assertEqual(1, device_mock.postFormWithParts.call_count) + self.assertEquals( + [call.createFormPart("name=\"file\"; filename=\"generic_pla_white.xml.fdm_material\"", ""), + call.postFormWithParts(target = "materials/", parts = ["_xXx_"], on_finished = job.sendingFinished)], + device_mock.method_calls) From f3338aa187bfe607664d0d79d5848d7cdee5fc06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijn=20De=C3=A9?= Date: Tue, 20 Nov 2018 16:53:01 +0100 Subject: [PATCH 042/146] Fixed the failing tests --- plugins/UM3NetworkPrinting/src/SendMaterialJob.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py index 0599101379..fe0cd98964 100644 --- a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py @@ -84,8 +84,9 @@ class SendMaterialJob(Job): def _determineMaterialsToSend(local_materials: Dict[str, LocalMaterial], remote_materials: Dict[str, ClusterMaterial]) -> Set[str]: return { - material.id for guid, material in local_materials.items() - if guid not in remote_materials or material.version > remote_materials[guid].version + material.id + for guid, material in local_materials.items() + if guid not in remote_materials or int(material.version) > remote_materials[guid].version } ## Send the materials to the printer. From 23744e42d1f4480fdc7f3c56f3591074b3ae8723 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Wed, 21 Nov 2018 09:15:18 +0100 Subject: [PATCH 043/146] Remove SaveButton file, since it's not used anymore. Contributes to CURA-5942 --- resources/qml/SaveButton.qml | 478 ----------------------------------- 1 file changed, 478 deletions(-) delete mode 100644 resources/qml/SaveButton.qml diff --git a/resources/qml/SaveButton.qml b/resources/qml/SaveButton.qml deleted file mode 100644 index c2d310e30c..0000000000 --- a/resources/qml/SaveButton.qml +++ /dev/null @@ -1,478 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Cura is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.7 -import QtQuick.Controls 1.1 -import QtQuick.Controls.Styles 1.1 -import QtQuick.Layouts 1.1 - -import UM 1.1 as UM -import Cura 1.0 as Cura - -// This widget does so much more than "just" being a save button, so it should be refactored at some point in time. -Item -{ - id: base; - UM.I18nCatalog { id: catalog; name: "cura"} - - property real progress: UM.Backend.progress - property int backendState: UM.Backend.state - property bool activity: CuraApplication.platformActivity - - property alias buttonRowWidth: saveRow.width - - property string fileBaseName - property string statusText: - { - if(!activity) - { - return catalog.i18nc("@label:PrintjobStatus", "Please load a 3D model"); - } - - switch(base.backendState) - { - case 1: - return catalog.i18nc("@label:PrintjobStatus", "Ready to slice"); - case 2: - return catalog.i18nc("@label:PrintjobStatus", "Slicing..."); - case 3: - return catalog.i18nc("@label:PrintjobStatus %1 is target operation", "Ready to %1").arg(UM.OutputDeviceManager.activeDeviceShortDescription); - case 4: - return catalog.i18nc("@label:PrintjobStatus", "Unable to Slice"); - case 5: - return catalog.i18nc("@label:PrintjobStatus", "Slicing unavailable"); - default: - return ""; - } - } - - function sliceOrStopSlicing() - { - try - { - if ([1, 5].indexOf(base.backendState) != -1) - { - CuraApplication.backend.forceSlice(); - } - else - { - CuraApplication.backend.stopSlicing(); - } - } - catch (e) - { - console.log("Could not start or stop slicing.", e) - } - } - - Label - { - id: statusLabel - width: parent.width - 2 * UM.Theme.getSize("thick_margin").width - anchors.top: parent.top - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("thick_margin").width - - color: UM.Theme.getColor("text") - font: UM.Theme.getFont("default_bold") - text: statusText; - } - - Rectangle - { - id: progressBar - width: parent.width - 2 * UM.Theme.getSize("thick_margin").width - height: UM.Theme.getSize("progressbar").height - anchors.top: statusLabel.bottom - anchors.topMargin: Math.round(UM.Theme.getSize("thick_margin").height / 4) - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("thick_margin").width - radius: UM.Theme.getSize("progressbar_radius").width - color: UM.Theme.getColor("progressbar_background") - - Rectangle - { - width: Math.max(parent.width * base.progress) - height: parent.height - color: UM.Theme.getColor("progressbar_control") - radius: UM.Theme.getSize("progressbar_radius").width - visible: base.backendState == 2 - } - } - - // Shortcut for "save as/print/..." - Action - { - shortcut: "Ctrl+P" - onTriggered: - { - // only work when the button is enabled - if (saveToButton.enabled) - { - saveToButton.clicked(); - } - // prepare button - if (prepareButton.enabled) - { - sliceOrStopSlicing(); - } - } - } - - Item - { - id: saveRow - width: { - // using childrenRect.width directly causes a binding loop, because setting the width affects the childrenRect - var children_width = UM.Theme.getSize("default_margin").width; - for (var index in children) - { - var child = children[index]; - if(child.visible) - { - children_width += child.width + child.anchors.rightMargin; - } - } - return Math.min(children_width, base.width - UM.Theme.getSize("thick_margin").width); - } - height: saveToButton.height - anchors.bottom: parent.bottom - anchors.bottomMargin: UM.Theme.getSize("thick_margin").height - anchors.right: parent.right - clip: true - - Row - { - id: additionalComponentsRow - anchors.top: parent.top - anchors.right: saveToButton.visible ? saveToButton.left : (prepareButton.visible ? prepareButton.left : parent.right) - anchors.rightMargin: UM.Theme.getSize("default_margin").width - - spacing: UM.Theme.getSize("default_margin").width - } - - Component.onCompleted: - { - saveRow.addAdditionalComponents("saveButton") - } - - Connections - { - target: CuraApplication - onAdditionalComponentsChanged: saveRow.addAdditionalComponents("saveButton") - } - - function addAdditionalComponents (areaId) - { - if(areaId == "saveButton") - { - for (var component in CuraApplication.additionalComponents["saveButton"]) - { - CuraApplication.additionalComponents["saveButton"][component].parent = additionalComponentsRow - } - } - } - - Connections - { - target: UM.Preferences - onPreferenceChanged: - { - var autoSlice = UM.Preferences.getValue("general/auto_slice"); - prepareButton.autoSlice = autoSlice; - saveToButton.autoSlice = autoSlice; - } - } - - // Prepare button, only shows if auto_slice is off - Button - { - id: prepareButton - - tooltip: [1, 5].indexOf(base.backendState) != -1 ? catalog.i18nc("@info:tooltip","Slice current printjob") : catalog.i18nc("@info:tooltip","Cancel slicing process") - // 1 = not started, 2 = Processing - enabled: ([1, 2].indexOf(base.backendState) != -1) && base.activity - visible: !autoSlice && ([1, 2, 4].indexOf(base.backendState) != -1) && base.activity - property bool autoSlice - height: UM.Theme.getSize("save_button_save_to_button").height - - anchors.top: parent.top - anchors.right: parent.right - anchors.rightMargin: UM.Theme.getSize("thick_margin").width - - // 1 = not started, 4 = error, 5 = disabled - text: [1, 4, 5].indexOf(base.backendState) != -1 ? catalog.i18nc("@label:Printjob", "Prepare") : catalog.i18nc("@label:Printjob", "Cancel") - onClicked: - { - sliceOrStopSlicing(); - } - - style: ButtonStyle - { - background: Rectangle - { - border.width: UM.Theme.getSize("default_lining").width - border.color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled_border"); - } - else if(control.pressed) - { - return UM.Theme.getColor("action_button_active_border"); - } - else if(control.hovered) - { - return UM.Theme.getColor("action_button_hovered_border"); - } - else - { - return UM.Theme.getColor("action_button_border"); - } - } - color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled"); - } - else if(control.pressed) - { - return UM.Theme.getColor("action_button_active"); - } - else if(control.hovered) - { - return UM.Theme.getColor("action_button_hovered"); - } - else - { - return UM.Theme.getColor("action_button"); - } - } - - Behavior on color { ColorAnimation { duration: 50; } } - - implicitWidth: actualLabel.contentWidth + (UM.Theme.getSize("thick_margin").width * 2) - - Label - { - id: actualLabel - anchors.centerIn: parent - color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled_text"); - } - else if(control.pressed) - { - return UM.Theme.getColor("action_button_active_text"); - } - else if(control.hovered) - { - return UM.Theme.getColor("action_button_hovered_text"); - } - else - { - return UM.Theme.getColor("action_button_text"); - } - } - font: UM.Theme.getFont("action_button") - text: control.text; - } - } - label: Item {} - } - } - - Button - { - id: saveToButton - - tooltip: UM.OutputDeviceManager.activeDeviceDescription; - // 3 = done, 5 = disabled - enabled: base.backendState != "undefined" && (base.backendState == 3 || base.backendState == 5) && base.activity == true - visible: base.backendState != "undefined" && autoSlice || ((base.backendState == 3 || base.backendState == 5) && base.activity == true) - property bool autoSlice - height: UM.Theme.getSize("save_button_save_to_button").height - - anchors.top: parent.top - anchors.right: deviceSelectionMenu.visible ? deviceSelectionMenu.left : parent.right - anchors.rightMargin: deviceSelectionMenu.visible ? -3 * UM.Theme.getSize("default_lining").width : UM.Theme.getSize("thick_margin").width - - text: UM.OutputDeviceManager.activeDeviceShortDescription - onClicked: - { - forceActiveFocus(); - UM.OutputDeviceManager.requestWriteToDevice(UM.OutputDeviceManager.activeDevice, PrintInformation.jobName, - { "filter_by_machine": true, "preferred_mimetypes": Cura.MachineManager.activeMachine.preferred_output_file_formats }); - } - - style: ButtonStyle - { - background: Rectangle - { - border.width: UM.Theme.getSize("default_lining").width - border.color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled_border"); - } - else if(control.pressed) - { - return UM.Theme.getColor("print_button_ready_pressed_border"); - } - else if(control.hovered) - { - return UM.Theme.getColor("print_button_ready_hovered_border"); - } - else - { - return UM.Theme.getColor("print_button_ready_border"); - } - } - color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled"); - } - else if(control.pressed) - { - return UM.Theme.getColor("print_button_ready_pressed"); - } - else if(control.hovered) - { - return UM.Theme.getColor("print_button_ready_hovered"); - } - else - { - return UM.Theme.getColor("print_button_ready"); - } - } - - Behavior on color { ColorAnimation { duration: 50; } } - - implicitWidth: actualLabel.contentWidth + (UM.Theme.getSize("thick_margin").width * 2) - - Label - { - id: actualLabel - anchors.centerIn: parent - color: control.enabled ? UM.Theme.getColor("print_button_ready_text") : UM.Theme.getColor("action_button_disabled_text") - font: UM.Theme.getFont("action_button") - text: control.text - } - } - label: Item { } - } - } - - Button - { - id: deviceSelectionMenu - tooltip: catalog.i18nc("@info:tooltip","Select the active output device"); - anchors.top: parent.top - anchors.right: parent.right - - anchors.rightMargin: UM.Theme.getSize("thick_margin").width - width: UM.Theme.getSize("save_button_save_to_button").height - height: UM.Theme.getSize("save_button_save_to_button").height - - // 3 = Done, 5 = Disabled - enabled: (base.backendState == 3 || base.backendState == 5) && base.activity == true - visible: (devicesModel.deviceCount > 1) && (base.backendState == 3 || base.backendState == 5) && base.activity == true - - - style: ButtonStyle - { - background: Rectangle - { - id: deviceSelectionIcon - border.width: UM.Theme.getSize("default_lining").width - border.color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled_border") - } - else if(control.pressed) - { - return UM.Theme.getColor("print_button_ready_pressed_border") - } - else if(control.hovered) - { - return UM.Theme.getColor("print_button_ready_hovered_border") - } - else - { - return UM.Theme.getColor("print_button_ready_border") - } - } - color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled") - } - else if(control.pressed) - { - return UM.Theme.getColor("print_button_ready_pressed") - } - else if(control.hovered) - { - return UM.Theme.getColor("print_button_ready_hovered") - } - else - { - return UM.Theme.getColor("print_button_ready") - } - } - Behavior on color { ColorAnimation { duration: 50; } } - anchors.left: parent.left - anchors.leftMargin: Math.round(UM.Theme.getSize("save_button_text_margin").width / 2); - width: parent.height - height: parent.height - - UM.RecolorImage - { - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - width: UM.Theme.getSize("standard_arrow").width - height: UM.Theme.getSize("standard_arrow").height - sourceSize.width: width - sourceSize.height: height - color: control.enabled ? UM.Theme.getColor("print_button_ready_text") : UM.Theme.getColor("action_button_disabled_text") - source: UM.Theme.getIcon("arrow_bottom") - } - } - } - - menu: Menu - { - id: devicesMenu; - Instantiator - { - model: devicesModel; - MenuItem - { - text: model.description - checkable: true; - checked: model.id == UM.OutputDeviceManager.activeDevice - exclusiveGroup: devicesMenuGroup - onTriggered: - { - UM.OutputDeviceManager.setActiveDevice(model.id); - } - } - onObjectAdded: devicesMenu.insertItem(index, object) - onObjectRemoved: devicesMenu.removeItem(object) - } - ExclusiveGroup { id: devicesMenuGroup } - } - } - UM.OutputDevicesModel { id: devicesModel } - } -} From 9e8be286af8736a97bf05db3b4bb5b0063be944b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijn=20De=C3=A9?= Date: Wed, 21 Nov 2018 10:12:53 +0100 Subject: [PATCH 044/146] Used NamedTuple from typing iso namedtuple from collections so we can at least give type hints --- plugins/UM3NetworkPrinting/src/Models.py | 54 +++++++++---------- .../UM3NetworkPrinting/src/SendMaterialJob.py | 51 +++++++++--------- .../tests/TestSendMaterialJob.py | 4 +- 3 files changed, 55 insertions(+), 54 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Models.py b/plugins/UM3NetworkPrinting/src/Models.py index a9210ac5b4..e84a39db5a 100644 --- a/plugins/UM3NetworkPrinting/src/Models.py +++ b/plugins/UM3NetworkPrinting/src/Models.py @@ -1,33 +1,33 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from collections import namedtuple +from typing import NamedTuple -ClusterMaterial = namedtuple('ClusterMaterial', [ - 'guid', - 'material', - 'brand', - 'version', - 'color', - 'density' +ClusterMaterial = NamedTuple("ClusterMaterial", [ + ("guid", str), + ("material", str), + ("brand", str), + ("version", int), + ("color", str), + ("density", str), ]) -LocalMaterial = namedtuple('LocalMaterial', [ - 'GUID', - 'id', - 'type', - 'status', - 'base_file', - 'setting_version', - 'version', - 'name', - 'brand', - 'material', - 'color_name', - 'color_code', - 'description', - 'adhesion_info', - 'approximate_diameter', - 'properties', - 'definition', - 'compatible' +LocalMaterial = NamedTuple("LocalMaterial", [ + ("GUID", str), + ("id", str), + ("type", str), + ("status", str), + ("base_file", str), + ("setting_version", int), + ("version", int), + ("name", str), + ("brand", str), + ("material", str), + ("color_name", str), + ("color_code", str), + ("description", str), + ("adhesion_info", str), + ("approximate_diameter", str), + ("properties", str), + ("definition", str), + ("compatible", str), ]) diff --git a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py index fe0cd98964..ee8dd8042d 100644 --- a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py @@ -2,7 +2,6 @@ # Cura is released under the terms of the LGPLv3 or higher. import json import os -import re import urllib.parse from typing import Dict, TYPE_CHECKING, Set @@ -13,13 +12,13 @@ from UM.Logger import Logger from UM.MimeTypeDatabase import MimeTypeDatabase from UM.Resources import Resources from cura.CuraApplication import CuraApplication - # Absolute imports don't work in plugins from .Models import ClusterMaterial, LocalMaterial if TYPE_CHECKING: from .ClusterUM3OutputDevice import ClusterUM3OutputDevice + ## Asynchronous job to send material profiles to the printer. # # This way it won't freeze up the interface while sending those materials. @@ -86,7 +85,7 @@ class SendMaterialJob(Job): return { material.id for guid, material in local_materials.items() - if guid not in remote_materials or int(material.version) > remote_materials[guid].version + if guid not in remote_materials or material.version > remote_materials[guid].version } ## Send the materials to the printer. @@ -122,23 +121,23 @@ class SendMaterialJob(Job): # \param material_id The ID of the material in the file. def _sendMaterialFile(self, file_path: str, file_name: str, material_id: str) -> None: - parts = [] + parts = [] - # Add the material file. - with open(file_path, "rb") as f: - parts.append(self.device.createFormPart("name=\"file\"; filename=\"{file_name}\"" - .format(file_name = file_name), f.read())) + # Add the material file. + with open(file_path, "rb") as f: + parts.append(self.device.createFormPart("name=\"file\"; filename=\"{file_name}\"" + .format(file_name = file_name), f.read())) - # Add the material signature file if needed. - signature_file_path = "{}.sig".format(file_path) - if os.path.exists(signature_file_path): - signature_file_name = os.path.basename(signature_file_path) - with open(signature_file_path, "rb") as f: - parts.append(self.device.createFormPart("name=\"signature_file\"; filename=\"{file_name}\"" - .format(file_name = signature_file_name), f.read())) + # Add the material signature file if needed. + signature_file_path = "{}.sig".format(file_path) + if os.path.exists(signature_file_path): + signature_file_name = os.path.basename(signature_file_path) + with open(signature_file_path, "rb") as f: + parts.append(self.device.createFormPart("name=\"signature_file\"; filename=\"{file_name}\"" + .format(file_name = signature_file_name), f.read())) - Logger.log("d", "Syncing material {material_id} with cluster.".format(material_id = material_id)) - self.device.postFormWithParts(target = "materials/", parts = parts, on_finished = self.sendingFinished) + Logger.log("d", "Syncing material {material_id} with cluster.".format(material_id = material_id)) + self.device.postFormWithParts(target = "materials/", parts = parts, on_finished = self.sendingFinished) ## Check a reply from an upload to the printer and log an error when the call failed @staticmethod @@ -174,16 +173,18 @@ class SendMaterialJob(Job): # Find the latest version of all material containers in the registry. for material in material_containers: try: - material = LocalMaterial(**material) - # material version must be an int - if not re.match("\d+", material.version): - Logger.logException("w", "Local material {} has invalid version '{}'." - .format(material["id"], material.version)) - continue + material["version"] = int(material["version"]) - if material.GUID not in result or material.version > result.get(material.GUID).version: - result[material.GUID] = material + # Create a new local material + local_material = LocalMaterial(**material) + + if local_material.GUID not in result or \ + local_material.version > result.get(local_material.GUID).version: + result[local_material.GUID] = local_material + + except KeyError: + Logger.logException("w", "Local material {} has missing values.".format(material["id"])) except ValueError: Logger.logException("w", "Local material {} has invalid values.".format(material["id"])) diff --git a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py index f4604580fe..ff896683e1 100644 --- a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py @@ -19,7 +19,7 @@ from plugins.UM3NetworkPrinting.src.SendMaterialJob import SendMaterialJob @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: ["/materials/generic_pla_white.xml.fdm_material"]) class TestSendMaterialJob(TestCase): _LOCAL_MATERIAL_WHITE = {"type": "material", "status": "unknown", "id": "generic_pla_white", - "base_file": "generic_pla_white", "setting_version": 5, "name": "White PLA", + "base_file": "generic_pla_white", "setting_version": "5", "name": "White PLA", "brand": "Generic", "material": "PLA", "color_name": "White", "GUID": "badb0ee7-87c8-4f3f-9398-938587b67dce", "version": "1", "color_code": "#ffffff", "description": "Test PLA White", "adhesion_info": "Use glue.", "approximate_diameter": "3", @@ -27,7 +27,7 @@ class TestSendMaterialJob(TestCase): "definition": "fdmprinter", "compatible": True} _LOCAL_MATERIAL_BLACK = {"type": "material", "status": "unknown", "id": "generic_pla_black", - "base_file": "generic_pla_black", "setting_version": 5, "name": "Yellow CPE", + "base_file": "generic_pla_black", "setting_version": "5", "name": "Yellow CPE", "brand": "Ultimaker", "material": "CPE", "color_name": "Black", "GUID": "5fbb362a-41f9-4818-bb43-15ea6df34aa4", "version": "1", "color_code": "#000000", "description": "Test PLA Black", "adhesion_info": "Use glue.", "approximate_diameter": "3", From 7b0f8882a2715d4beb9378646bc915e77c8f4963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijn=20De=C3=A9?= Date: Wed, 21 Nov 2018 11:01:26 +0100 Subject: [PATCH 045/146] Reverted models to namedtuples from collections because NamedTuple is a Python3.6 feature --- plugins/UM3NetworkPrinting/src/Models.py | 54 ++++++++++++------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Models.py b/plugins/UM3NetworkPrinting/src/Models.py index e84a39db5a..d5e1007555 100644 --- a/plugins/UM3NetworkPrinting/src/Models.py +++ b/plugins/UM3NetworkPrinting/src/Models.py @@ -1,33 +1,33 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import NamedTuple +from collections import namedtuple -ClusterMaterial = NamedTuple("ClusterMaterial", [ - ("guid", str), - ("material", str), - ("brand", str), - ("version", int), - ("color", str), - ("density", str), +ClusterMaterial = namedtuple('ClusterMaterial', [ + 'guid', # Type: str + 'material', # Type: str + 'brand', # Type: str + 'version', # Type: int + 'color', # Type: str + 'density' # Type: str ]) -LocalMaterial = NamedTuple("LocalMaterial", [ - ("GUID", str), - ("id", str), - ("type", str), - ("status", str), - ("base_file", str), - ("setting_version", int), - ("version", int), - ("name", str), - ("brand", str), - ("material", str), - ("color_name", str), - ("color_code", str), - ("description", str), - ("adhesion_info", str), - ("approximate_diameter", str), - ("properties", str), - ("definition", str), - ("compatible", str), +LocalMaterial = namedtuple('LocalMaterial', [ + 'GUID', # Type: str + 'id', # Type: str + 'type', # Type: str + 'status', # Type: str + 'base_file', # Type: str + 'setting_version', # Type: int + 'version', # Type: int + 'name', # Type: str + 'brand', # Type: str + 'material', # Type: str + 'color_name', # Type: str + 'color_code', # Type: str + 'description', # Type: str + 'adhesion_info', # Type: str + 'approximate_diameter', # Type: str + 'properties', # Type: str + 'definition', # Type: str + 'compatible' # Type: str ]) From 8e47d0475632296030f9e2d1f49c6d8797b93982 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Wed, 21 Nov 2018 11:12:59 +0100 Subject: [PATCH 046/146] Adjust sizes and alignments to the print selector panel. Contributes to CURA-5942. --- plugins/PrepareStage/PrepareMenu.qml | 2 +- resources/qml/ExpandableComponent.qml | 19 +++++++++++++++--- resources/qml/MachineSelector.qml | 27 +++++++++++++------------- resources/themes/cura-light/theme.json | 9 +++++---- 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/plugins/PrepareStage/PrepareMenu.qml b/plugins/PrepareStage/PrepareMenu.qml index ef01625a22..121928e19f 100644 --- a/plugins/PrepareStage/PrepareMenu.qml +++ b/plugins/PrepareStage/PrepareMenu.qml @@ -46,7 +46,7 @@ Item anchors.centerIn: parent - width: 0.9 * prepareMenu.width + width: Math.round(0.9 * prepareMenu.width) height: parent.height spacing: 0 diff --git a/resources/qml/ExpandableComponent.qml b/resources/qml/ExpandableComponent.qml index 8ed6dc5674..c1e3bc7985 100644 --- a/resources/qml/ExpandableComponent.qml +++ b/resources/qml/ExpandableComponent.qml @@ -10,6 +10,13 @@ import UM 1.2 as UM Item { id: base + + // Enumeration with the different possible alignments of the popup with respect of the headerItem + enum PopupAlignment { + AlignLeft, + AlignRight + } + // The headerItem holds the QML item that is always displayed. property alias headerItem: headerItemLoader.sourceComponent @@ -21,6 +28,9 @@ Item property color headerBackgroundColor: UM.Theme.getColor("action_button") property color headerHoverColor: UM.Theme.getColor("action_button_hovered") + // Defines the alignment of the popup with respect of the headerItem, by default to the right + property int popupAlignment: ExpandableComponent.PopupAlignment.AlignRight + // How much spacing is needed around the popupItem property alias popupPadding: popup.padding @@ -42,6 +52,7 @@ Item function togglePopup() { +// print(popupAlignment, popupAlignment == PopupAlignment.AlignRight) if(popup.visible) { popup.close() @@ -116,8 +127,8 @@ Item sourceSize.height: height visible: source != "" width: height - height: 0.2 * base.height - color: "black" + height: Math.round(0.2 * base.height) + color: UM.Theme.getColor("text") } MouseArea @@ -140,13 +151,15 @@ Item // Make the popup right aligned with the rest. The 3x padding is due to left, right and padding between // the button & text. - x: -width + collapseButton.width + headerItemLoader.width + 3 * background.padding + x: popupAlignment == ExpandableComponent.PopupAlignment.AlignRight ? -width + collapseButton.width + headerItemLoader.width + 3 * background.padding : 0 padding: UM.Theme.getSize("default_margin").width closePolicy: Popup.CloseOnPressOutsideParent background: Rectangle { color: popupBackgroundColor + border.width: UM.Theme.getSize("default_lining").width + border.color: UM.Theme.getColor("lining") } } } diff --git a/resources/qml/MachineSelector.qml b/resources/qml/MachineSelector.qml index c9756d93ba..f029914884 100644 --- a/resources/qml/MachineSelector.qml +++ b/resources/qml/MachineSelector.qml @@ -17,6 +17,8 @@ Cura.ExpandableComponent property bool isNetworkPrinter: Cura.MachineManager.activeMachineNetworkKey != "" + popupPadding: 0 + popupAlignment: ExpandableComponent.PopupAlignment.AlignLeft iconSource: expanded ? UM.Theme.getIcon("arrow_bottom") : UM.Theme.getIcon("arrow_left") UM.I18nCatalog @@ -31,6 +33,7 @@ Cura.ExpandableComponent verticalAlignment: Text.AlignVCenter height: parent.height elide: Text.ElideRight + renderType: Text.NativeRendering font: UM.Theme.getFont("default") color: UM.Theme.getColor("text") } @@ -38,26 +41,25 @@ Cura.ExpandableComponent popupItem: Item { id: popup - width: machineSelector.width - 2 * UM.Theme.getSize("default_margin").width - height: 200 + width: UM.Theme.getSize("machine_selector_widget_content").width + height: UM.Theme.getSize("machine_selector_widget_content").height ScrollView { anchors.fill: parent - contentHeight: column.implicitHeight - contentWidth: column.implicitWidth clip: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff Column { id: column anchors.fill: parent + Label { - text: catalog.i18nc("@label", "Networked Printers") + text: catalog.i18nc("@label", "Network connected printers") visible: networkedPrintersModel.items.length > 0 height: visible ? contentHeight + 2 * UM.Theme.getSize("default_margin").height : 0 + renderType: Text.NativeRendering font: UM.Theme.getFont("medium_bold") color: UM.Theme.getColor("text") verticalAlignment: Text.AlignVCenter @@ -73,13 +75,12 @@ Cura.ExpandableComponent filter: {"type": "machine", "um_network_key": "*", "hidden": "False"} } - delegate: RoundButton + delegate: Button { text: name width: parent.width - checkable: true - radius: UM.Theme.getSize("default_radius").width + onClicked: { togglePopup() @@ -92,14 +93,14 @@ Cura.ExpandableComponent onActiveMachineNetworkGroupNameChanged: checked = Cura.MachineManager.activeMachineNetworkGroupName == model.metadata["connect_group_name"] } } - } Label { - text: catalog.i18nc("@label", "Virtual Printers") + text: catalog.i18nc("@label", "Preset printers") visible: virtualPrintersModel.items.length > 0 height: visible ? contentHeight + 2 * UM.Theme.getSize("default_margin").height : 0 + renderType: Text.NativeRendering font: UM.Theme.getFont("medium_bold") color: UM.Theme.getColor("text") verticalAlignment: Text.AlignVCenter @@ -115,21 +116,19 @@ Cura.ExpandableComponent filter: {"type": "machine", "um_network_key": null} } - delegate: RoundButton + delegate: Button { text: name width: parent.width checked: Cura.MachineManager.activeMachineId == model.id checkable: true - radius: UM.Theme.getSize("default_radius").width onClicked: { togglePopup() Cura.MachineManager.setActiveMachine(model.id) } } - } } } diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index d28611529b..5f52adff14 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -367,11 +367,11 @@ "sizes": { "window_minimum_size": [106, 66], - "main_window_header": [0.0, 4.5], + "main_window_header": [0.0, 4.0], "main_window_header_button": [8, 2.35], "main_window_header_button_icon": [1.2, 1.2], - "stage_menu": [0.0, 4.5], + "stage_menu": [0.0, 4.0], "account_button": [12, 3], @@ -380,14 +380,15 @@ "print_setup_item": [0.0, 2.0], "print_setup_extruder_box": [0.0, 6.0], - "configuration_selector_widget": [35.0, 4.5], + "configuration_selector_widget": [35.0, 4.0], "configuration_selector_mode_tabs": [0.0, 3.0], "action_panel_widget": [25.0, 0.0], "action_panel_information_widget": [20.0, 0.0], "action_panel_button": [15.0, 3.0], - "machine_selector_widget": [16.0, 4.5], + "machine_selector_widget": [20.0, 4.0], + "machine_selector_widget_content": [25.0, 32.0], "views_selector": [0.0, 4.0], From de650300e20abf2a54baa953076bb71cf2943e1b Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Wed, 21 Nov 2018 11:43:20 +0100 Subject: [PATCH 047/146] Add the buttons to add printer and manage printers in the bottom of the print selector panel. Contributes to CURA-5942. --- resources/qml/ExpandableComponent.qml | 6 ++-- resources/qml/MachineSelector.qml | 52 +++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/resources/qml/ExpandableComponent.qml b/resources/qml/ExpandableComponent.qml index c1e3bc7985..aa9e1467fa 100644 --- a/resources/qml/ExpandableComponent.qml +++ b/resources/qml/ExpandableComponent.qml @@ -52,7 +52,6 @@ Item function togglePopup() { -// print(popupAlignment, popupAlignment == PopupAlignment.AlignRight) if(popup.visible) { popup.close() @@ -149,8 +148,8 @@ Item // Ensure that the popup is located directly below the headerItem y: headerItemLoader.height + 2 * background.padding - // Make the popup right aligned with the rest. The 3x padding is due to left, right and padding between - // the button & text. + // Make the popup aligned with the rest, using the property popupAlignment to decide whether is right or left. + // In case of right alignment, the 3x padding is due to left, right and padding between the button & text. x: popupAlignment == ExpandableComponent.PopupAlignment.AlignRight ? -width + collapseButton.width + headerItemLoader.width + 3 * background.padding : 0 padding: UM.Theme.getSize("default_margin").width closePolicy: Popup.CloseOnPressOutsideParent @@ -160,6 +159,7 @@ Item color: popupBackgroundColor border.width: UM.Theme.getSize("default_lining").width border.color: UM.Theme.getColor("lining") + radius: UM.Theme.getSize("default_radius").width } } } diff --git a/resources/qml/MachineSelector.qml b/resources/qml/MachineSelector.qml index f029914884..3e70fda299 100644 --- a/resources/qml/MachineSelector.qml +++ b/resources/qml/MachineSelector.qml @@ -1,7 +1,7 @@ // Copyright (c) 2018 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. -import QtQuick 2.2 +import QtQuick 2.7 import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.1 import QtQuick.Layouts 1.1 @@ -46,7 +46,9 @@ Cura.ExpandableComponent ScrollView { - anchors.fill: parent + width: parent.width + anchors.top: parent.top + anchors.bottom: separator.top clip: true Column @@ -80,7 +82,7 @@ Cura.ExpandableComponent text: name width: parent.width checkable: true - + onClicked: { togglePopup() @@ -132,5 +134,49 @@ Cura.ExpandableComponent } } } + + Rectangle + { + id: separator + + anchors.bottom: buttonRow.top + width: parent.width + height: UM.Theme.getSize("default_lining").height + color: UM.Theme.getColor("lining") + } + + Row + { + id: buttonRow + + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + padding: UM.Theme.getSize("default_margin").width + spacing: UM.Theme.getSize("default_margin").width + + Cura.ActionButton + { + leftPadding: UM.Theme.getSize("default_margin").width + rightPadding: UM.Theme.getSize("default_margin").width + text: catalog.i18nc("@button", "Add printer") + color: UM.Theme.getColor("secondary") + hoverColor: UM.Theme.getColor("secondary") + textColor: UM.Theme.getColor("primary") + textHoverColor: UM.Theme.getColor("text") + onClicked: Cura.Actions.addMachine.trigger() + } + + Cura.ActionButton + { + leftPadding: UM.Theme.getSize("default_margin").width + rightPadding: UM.Theme.getSize("default_margin").width + text: catalog.i18nc("@button", "Manage printers") + color: UM.Theme.getColor("secondary") + hoverColor: UM.Theme.getColor("secondary") + textColor: UM.Theme.getColor("primary") + textHoverColor: UM.Theme.getColor("text") + onClicked: Cura.Actions.configureMachines.trigger() + } + } } } From 64bbab9d4024df8d1be956040d81a69551922308 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Wed, 21 Nov 2018 11:50:00 +0100 Subject: [PATCH 048/146] Use the group name to show in the printer list if it's a network connected printer. Contributes to CURA-5942. --- resources/qml/MachineSelector.qml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/resources/qml/MachineSelector.qml b/resources/qml/MachineSelector.qml index 3e70fda299..417c5722b4 100644 --- a/resources/qml/MachineSelector.qml +++ b/resources/qml/MachineSelector.qml @@ -79,8 +79,9 @@ Cura.ExpandableComponent delegate: Button { - text: name + text: model.metadata["connect_group_name"] width: parent.width + checked: Cura.MachineManager.activeMachineNetworkGroupName == model.metadata["connect_group_name"] checkable: true onClicked: @@ -120,7 +121,7 @@ Cura.ExpandableComponent delegate: Button { - text: name + text: model.name width: parent.width checked: Cura.MachineManager.activeMachineId == model.id checkable: true From 406fac9e605578422417cdbb7a666343a4260129 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Wed, 21 Nov 2018 13:38:08 +0100 Subject: [PATCH 049/146] Remove duplicate entries of renderType. Contributes to CURA-5942. --- resources/qml/MachineSelector.qml | 3 --- 1 file changed, 3 deletions(-) diff --git a/resources/qml/MachineSelector.qml b/resources/qml/MachineSelector.qml index 1796df5678..417c5722b4 100644 --- a/resources/qml/MachineSelector.qml +++ b/resources/qml/MachineSelector.qml @@ -36,7 +36,6 @@ Cura.ExpandableComponent renderType: Text.NativeRendering font: UM.Theme.getFont("default") color: UM.Theme.getColor("text") - renderType: Text.NativeRendering } popupItem: Item @@ -65,7 +64,6 @@ Cura.ExpandableComponent renderType: Text.NativeRendering font: UM.Theme.getFont("medium_bold") color: UM.Theme.getColor("text") - renderType: Text.NativeRendering verticalAlignment: Text.AlignVCenter } @@ -109,7 +107,6 @@ Cura.ExpandableComponent font: UM.Theme.getFont("medium_bold") color: UM.Theme.getColor("text") verticalAlignment: Text.AlignVCenter - renderType: Text.NativeRendering } Repeater From fe7d1825d4f124788e9fc0ec19db6528fae388c3 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Wed, 21 Nov 2018 16:54:57 +0100 Subject: [PATCH 050/146] Add styling to the buttons in the printer list. Contributes to CURA-5942. --- plugins/PrepareStage/PrepareMenu.qml | 2 +- resources/qml/MachineSelector.qml | 34 +++++++++++++++---- .../cura-light/images/header_pattern.svg | 1 + resources/themes/cura-light/theme.json | 1 + 4 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 resources/themes/cura-light/images/header_pattern.svg diff --git a/plugins/PrepareStage/PrepareMenu.qml b/plugins/PrepareStage/PrepareMenu.qml index fd296144c7..eed1da0ad8 100644 --- a/plugins/PrepareStage/PrepareMenu.qml +++ b/plugins/PrepareStage/PrepareMenu.qml @@ -91,7 +91,7 @@ Item radius: UM.Theme.getSize("default_radius").width color: UM.Theme.getColor("toolbar_background") - + Button { id: openFileButton diff --git a/resources/qml/MachineSelector.qml b/resources/qml/MachineSelector.qml index 417c5722b4..a6339e2621 100644 --- a/resources/qml/MachineSelector.qml +++ b/resources/qml/MachineSelector.qml @@ -46,6 +46,7 @@ Cura.ExpandableComponent ScrollView { + id: scroll width: parent.width anchors.top: parent.top anchors.bottom: separator.top @@ -54,16 +55,20 @@ Cura.ExpandableComponent Column { id: column - anchors.fill: parent + + // Can't use parent.width since the parent is the flickable component and not the ScrollView + width: scroll.width - 2 * UM.Theme.getSize("default_lining").width + x: UM.Theme.getSize("default_lining").width Label { text: catalog.i18nc("@label", "Network connected printers") visible: networkedPrintersModel.items.length > 0 + leftPadding: UM.Theme.getSize("default_margin").width height: visible ? contentHeight + 2 * UM.Theme.getSize("default_margin").height : 0 renderType: Text.NativeRendering - font: UM.Theme.getFont("medium_bold") - color: UM.Theme.getColor("text") + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text_medium") verticalAlignment: Text.AlignVCenter } @@ -77,13 +82,20 @@ Cura.ExpandableComponent filter: {"type": "machine", "um_network_key": "*", "hidden": "False"} } - delegate: Button + delegate: Cura.ActionButton { text: model.metadata["connect_group_name"] width: parent.width + height: UM.Theme.getSize("action_button").height checked: Cura.MachineManager.activeMachineNetworkGroupName == model.metadata["connect_group_name"] checkable: true + color: "transparent" + hoverColor: UM.Theme.getColor("action_button_hovered") + textColor: UM.Theme.getColor("text") + textHoverColor: UM.Theme.getColor("text") + outlineColor: checked ? UM.Theme.getColor("primary") : "transparent" + onClicked: { togglePopup() @@ -102,10 +114,11 @@ Cura.ExpandableComponent { text: catalog.i18nc("@label", "Preset printers") visible: virtualPrintersModel.items.length > 0 + leftPadding: UM.Theme.getSize("default_margin").width height: visible ? contentHeight + 2 * UM.Theme.getSize("default_margin").height : 0 renderType: Text.NativeRendering - font: UM.Theme.getFont("medium_bold") - color: UM.Theme.getColor("text") + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text_medium") verticalAlignment: Text.AlignVCenter } @@ -119,13 +132,20 @@ Cura.ExpandableComponent filter: {"type": "machine", "um_network_key": null} } - delegate: Button + delegate: Cura.ActionButton { text: model.name width: parent.width + height: UM.Theme.getSize("action_button").height checked: Cura.MachineManager.activeMachineId == model.id checkable: true + color: "transparent" + hoverColor: UM.Theme.getColor("action_button_hovered") + textColor: UM.Theme.getColor("text") + textHoverColor: UM.Theme.getColor("text") + outlineColor: checked ? UM.Theme.getColor("primary") : "transparent" + onClicked: { togglePopup() diff --git a/resources/themes/cura-light/images/header_pattern.svg b/resources/themes/cura-light/images/header_pattern.svg new file mode 100644 index 0000000000..2a9de2f3e9 --- /dev/null +++ b/resources/themes/cura-light/images/header_pattern.svg @@ -0,0 +1 @@ +Pattern \ No newline at end of file diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 5f52adff14..4e6fe0776a 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -521,6 +521,7 @@ "avatar_image": [6.8, 6.8], + "action_button": [15.0, 3.0], "action_button_radius": [0.15, 0.15], "monitor_config_override_box": [1.0, 14.0], From 7e3f86f0913b7432c46b20507e33be57b7a1d514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijn=20De=C3=A9?= Date: Thu, 22 Nov 2018 09:37:47 +0100 Subject: [PATCH 051/146] Moved some of the mocks to class level because they are used in every test method --- .../tests/TestSendMaterialJob.py | 35 ++++++------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py index ff896683e1..548704fd33 100644 --- a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py @@ -17,6 +17,8 @@ from plugins.UM3NetworkPrinting.src.SendMaterialJob import SendMaterialJob lambda _: MimeType(name = "application/x-ultimaker-material-profile", comment = "Ultimaker Material Profile", suffixes = ["xml.fdm_material"])) @patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: ["/materials/generic_pla_white.xml.fdm_material"]) +@patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") +@patch("PyQt5.QtNetwork.QNetworkReply") class TestSendMaterialJob(TestCase): _LOCAL_MATERIAL_WHITE = {"type": "material", "status": "unknown", "id": "generic_pla_white", "base_file": "generic_pla_white", "setting_version": "5", "name": "White PLA", @@ -52,16 +54,13 @@ class TestSendMaterialJob(TestCase): "density": 1.00 } - @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") - def test_run(self, device_mock): + def test_run(self, device_mock, reply_mock): job = SendMaterialJob(device_mock) job.run() # We expect the materials endpoint to be called when the job runs. device_mock.get.assert_called_with("materials/", on_finished = job._onGetRemoteMaterials) - @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") - @patch("PyQt5.QtNetwork.QNetworkReply") def test__onGetRemoteMaterials_withFailedRequest(self, reply_mock, device_mock): reply_mock.attribute.return_value = 404 job = SendMaterialJob(device_mock) @@ -71,8 +70,6 @@ class TestSendMaterialJob(TestCase): self.assertEqual([call.attribute(0), call.errorString()], reply_mock.method_calls) self.assertEqual(0, device_mock.createFormPart.call_count) - @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") - @patch("PyQt5.QtNetwork.QNetworkReply") def test__onGetRemoteMaterials_withBadJsonAnswer(self, reply_mock, device_mock): reply_mock.attribute.return_value = 200 reply_mock.readAll.return_value = QByteArray(b"Six sick hicks nick six slick bricks with picks and sticks.") @@ -84,8 +81,6 @@ class TestSendMaterialJob(TestCase): self.assertEqual([call.attribute(0), call.readAll()], reply_mock.method_calls) self.assertEqual(0, device_mock.createFormPart.call_count) - @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") - @patch("PyQt5.QtNetwork.QNetworkReply") def test__onGetRemoteMaterials_withMissingGuidInRemoteMaterial(self, reply_mock, device_mock): reply_mock.attribute.return_value = 200 remote_material_without_guid = self._REMOTE_MATERIAL_WHITE.copy() @@ -101,10 +96,8 @@ class TestSendMaterialJob(TestCase): @patch("cura.Settings.CuraContainerRegistry") @patch("cura.CuraApplication") - @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") - @patch("PyQt5.QtNetwork.QNetworkReply") - def test__onGetRemoteMaterials_withInvalidVersionInLocalMaterial(self, reply_mock, device_mock, application_mock, - container_registry_mock): + def test__onGetRemoteMaterials_withInvalidVersionInLocalMaterial(self, application_mock, container_registry_mock, + reply_mock, device_mock): reply_mock.attribute.return_value = 200 reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii")) @@ -125,10 +118,8 @@ class TestSendMaterialJob(TestCase): @patch("cura.Settings.CuraContainerRegistry") @patch("cura.CuraApplication") - @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") - @patch("PyQt5.QtNetwork.QNetworkReply") - def test__onGetRemoteMaterials_withNoUpdate(self, reply_mock, device_mock, application_mock, - container_registry_mock): + def test__onGetRemoteMaterials_withNoUpdate(self, application_mock, container_registry_mock, reply_mock, + device_mock): application_mock.getContainerRegistry.return_value = container_registry_mock device_mock.createFormPart.return_value = "_xXx_" @@ -150,10 +141,8 @@ class TestSendMaterialJob(TestCase): @patch("cura.Settings.CuraContainerRegistry") @patch("cura.CuraApplication") - @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") - @patch("PyQt5.QtNetwork.QNetworkReply") - def test__onGetRemoteMaterials_withUpdatedMaterial(self, reply_mock, device_mock, application_mock, - container_registry_mock): + def test__onGetRemoteMaterials_withUpdatedMaterial(self, application_mock, container_registry_mock, reply_mock, + device_mock): application_mock.getContainerRegistry.return_value = container_registry_mock device_mock.createFormPart.return_value = "_xXx_" @@ -181,10 +170,8 @@ class TestSendMaterialJob(TestCase): @patch("cura.Settings.CuraContainerRegistry") @patch("cura.CuraApplication") - @patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice") - @patch("PyQt5.QtNetwork.QNetworkReply") - def test__onGetRemoteMaterials_withNewMaterial(self, reply_mock, device_mock, application_mock, - container_registry_mock): + def test__onGetRemoteMaterials_withNewMaterial(self, application_mock, container_registry_mock, reply_mock, + device_mock): application_mock.getContainerRegistry.return_value = container_registry_mock device_mock.createFormPart.return_value = "_xXx_" From 352427e4601204851daba848599153ebab3f8c41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijn=20De=C3=A9?= Date: Thu, 22 Nov 2018 10:01:15 +0100 Subject: [PATCH 052/146] Moved exception handling closer to the cause of error --- plugins/UM3NetworkPrinting/src/SendMaterialJob.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py index ee8dd8042d..72269040e7 100644 --- a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py @@ -46,9 +46,8 @@ class SendMaterialJob(Job): # Collect materials from the printer's reply and send the missing ones if needed. try: remote_materials_by_guid = self._parseReply(reply) - self._sendMissingMaterials(remote_materials_by_guid) - except json.JSONDecodeError: - Logger.logException("w", "Error parsing materials from printer") + if remote_materials_by_guid: + self._sendMissingMaterials(remote_materials_by_guid) except TypeError: Logger.logException("w", "Error parsing materials from printer") @@ -153,12 +152,14 @@ class SendMaterialJob(Job): # Parses the reply to a "/materials" request to the printer # # \return a dictionary of ClusterMaterial objects by GUID - # \throw json.JSONDecodeError Raised when the reply does not contain a valid json string # \throw KeyError Raised when on of the materials does not include a valid guid @classmethod def _parseReply(cls, reply: QNetworkReply) -> Dict[str, ClusterMaterial]: - remote_materials = json.loads(reply.readAll().data().decode("utf-8")) - return {material["guid"]: ClusterMaterial(**material) for material in remote_materials} + try: + remote_materials = json.loads(reply.readAll().data().decode("utf-8")) + return {material["guid"]: ClusterMaterial(**material) for material in remote_materials} + except json.JSONDecodeError: + Logger.logException("w", "Error parsing materials from printer") ## Retrieves a list of local materials # From b890e40e81f62da2eade839ad48d7168cdcde7ff Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Thu, 22 Nov 2018 10:54:25 +0100 Subject: [PATCH 053/146] Close the popup panel when the user clicks in some of the buttons in the printer selector. Contributes to CURA-5942. --- resources/qml/ExtruderIcon.qml | 1 + resources/qml/MachineSelector.qml | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/resources/qml/ExtruderIcon.qml b/resources/qml/ExtruderIcon.qml index c103ee245c..8f312adb85 100644 --- a/resources/qml/ExtruderIcon.qml +++ b/resources/qml/ExtruderIcon.qml @@ -16,6 +16,7 @@ Item property color materialColor property alias textColor: extruderNumberText.color property bool extruderEnabled: true + UM.RecolorImage { id: mainIcon diff --git a/resources/qml/MachineSelector.qml b/resources/qml/MachineSelector.qml index a6339e2621..14e1ebb48e 100644 --- a/resources/qml/MachineSelector.qml +++ b/resources/qml/MachineSelector.qml @@ -184,7 +184,11 @@ Cura.ExpandableComponent hoverColor: UM.Theme.getColor("secondary") textColor: UM.Theme.getColor("primary") textHoverColor: UM.Theme.getColor("text") - onClicked: Cura.Actions.addMachine.trigger() + onClicked: + { + togglePopup() + Cura.Actions.addMachine.trigger() + } } Cura.ActionButton @@ -196,7 +200,11 @@ Cura.ExpandableComponent hoverColor: UM.Theme.getColor("secondary") textColor: UM.Theme.getColor("primary") textHoverColor: UM.Theme.getColor("text") - onClicked: Cura.Actions.configureMachines.trigger() + onClicked: + { + togglePopup() + Cura.Actions.configureMachines.trigger() + } } } } From a1613c7f816c0d9c10659972f32bbed3a1995ea1 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 22 Nov 2018 13:53:27 +0100 Subject: [PATCH 054/146] Set the variable types of the Action button to the right kind. Because of boyscouting; these must be colors (and not generic vars) --- resources/qml/ActionButton.qml | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/resources/qml/ActionButton.qml b/resources/qml/ActionButton.qml index 2a8b894867..69d65e1c3f 100644 --- a/resources/qml/ActionButton.qml +++ b/resources/qml/ActionButton.qml @@ -15,15 +15,17 @@ Button property alias textFont: buttonText.font property alias cornerRadius: backgroundRect.radius property alias tooltip: tooltip.text - property var color: UM.Theme.getColor("primary") - property var hoverColor: UM.Theme.getColor("primary_hover") - property var disabledColor: color - property var textColor: UM.Theme.getColor("button_text") - property var textHoverColor: UM.Theme.getColor("button_text_hover") - property var textDisabledColor: textColor - property var outlineColor: color - property var outlineHoverColor: hoverColor - property var outlineDisabledColor: outlineColor + + property color color: UM.Theme.getColor("primary") + property color hoverColor: UM.Theme.getColor("primary_hover") + property color disabledColor: color + property color textColor: UM.Theme.getColor("button_text") + property color textHoverColor: UM.Theme.getColor("button_text_hover") + property color textDisabledColor: textColor + property color outlineColor: color + property color outlineHoverColor: hoverColor + property color outlineDisabledColor: outlineColor + // This property is used to indicate whether the button has a fixed width or the width would depend on the contents // Be careful when using fixedWidthMode, the translated texts can be too long that they won't fit. In any case, // we elide the text to the right so the text will be cut off with the three dots at the end. @@ -80,6 +82,7 @@ Button { id: mouseArea anchors.fill: parent + // Ensure that the button will still accept the clicks on it's own. onPressed: mouse.accepted = false hoverEnabled: true } From 9720512f50d87de2b6149f1d2475704309f3c07d Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Thu, 22 Nov 2018 13:54:10 +0100 Subject: [PATCH 055/146] Add a new printer selector button that is connected to the output devices and shows labels containing the type of printers that are in the same group. Contributes to CURA-5942. --- cura/PrinterOutputDevice.py | 5 + .../ConfigurationListView.qml | 2 +- .../{ => PrinterSelector}/MachineSelector.qml | 38 +----- .../PrinterSelector/MachineSelectorButton.qml | 110 ++++++++++++++++++ resources/qml/qmldir | 3 +- 5 files changed, 121 insertions(+), 37 deletions(-) rename resources/qml/{ => PrinterSelector}/MachineSelector.qml (79%) create mode 100644 resources/qml/PrinterSelector/MachineSelectorButton.qml diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index 969aa3c460..f8a663f0e4 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -211,6 +211,11 @@ class PrinterOutputDevice(QObject, OutputDevice): self._unique_configurations.sort(key = lambda k: k.printerType) self.uniqueConfigurationsChanged.emit() + # Returns the unique configurations of the printers within this output device + @pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged) + def uniquePrinterTypes(self) -> List[str]: + return list(set([configuration.printerType for configuration in self._unique_configurations])) + def _onPrintersChanged(self) -> None: for printer in self._printers: printer.configurationChanged.connect(self._updateUniqueConfigurations) diff --git a/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml b/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml index 7aaf87b4df..210ff6057f 100644 --- a/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml +++ b/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml @@ -21,7 +21,7 @@ Column { // FIXME For now the model should be removed and then created again, otherwise changes in the printer don't automatically update the UI configurationList.model = [] - if(outputDevice) + if (outputDevice) { configurationList.model = outputDevice.uniqueConfigurations } diff --git a/resources/qml/MachineSelector.qml b/resources/qml/PrinterSelector/MachineSelector.qml similarity index 79% rename from resources/qml/MachineSelector.qml rename to resources/qml/PrinterSelector/MachineSelector.qml index 14e1ebb48e..9280c45cf4 100644 --- a/resources/qml/MachineSelector.qml +++ b/resources/qml/PrinterSelector/MachineSelector.qml @@ -8,8 +8,6 @@ import QtQuick.Layouts 1.1 import UM 1.2 as UM import Cura 1.0 as Cura -import "Menus" - Cura.ExpandableComponent { @@ -18,7 +16,7 @@ Cura.ExpandableComponent property bool isNetworkPrinter: Cura.MachineManager.activeMachineNetworkKey != "" popupPadding: 0 - popupAlignment: ExpandableComponent.PopupAlignment.AlignLeft + popupAlignment: Cura.ExpandableComponent.PopupAlignment.AlignLeft iconSource: expanded ? UM.Theme.getIcon("arrow_bottom") : UM.Theme.getIcon("arrow_left") UM.I18nCatalog @@ -82,25 +80,10 @@ Cura.ExpandableComponent filter: {"type": "machine", "um_network_key": "*", "hidden": "False"} } - delegate: Cura.ActionButton + delegate: MachineSelectorButton { text: model.metadata["connect_group_name"] - width: parent.width - height: UM.Theme.getSize("action_button").height checked: Cura.MachineManager.activeMachineNetworkGroupName == model.metadata["connect_group_name"] - checkable: true - - color: "transparent" - hoverColor: UM.Theme.getColor("action_button_hovered") - textColor: UM.Theme.getColor("text") - textHoverColor: UM.Theme.getColor("text") - outlineColor: checked ? UM.Theme.getColor("primary") : "transparent" - - onClicked: - { - togglePopup() - Cura.MachineManager.setActiveMachine(model.id) - } Connections { @@ -132,25 +115,10 @@ Cura.ExpandableComponent filter: {"type": "machine", "um_network_key": null} } - delegate: Cura.ActionButton + delegate: MachineSelectorButton { text: model.name - width: parent.width - height: UM.Theme.getSize("action_button").height checked: Cura.MachineManager.activeMachineId == model.id - checkable: true - - color: "transparent" - hoverColor: UM.Theme.getColor("action_button_hovered") - textColor: UM.Theme.getColor("text") - textHoverColor: UM.Theme.getColor("text") - outlineColor: checked ? UM.Theme.getColor("primary") : "transparent" - - onClicked: - { - togglePopup() - Cura.MachineManager.setActiveMachine(model.id) - } } } } diff --git a/resources/qml/PrinterSelector/MachineSelectorButton.qml b/resources/qml/PrinterSelector/MachineSelectorButton.qml new file mode 100644 index 0000000000..5ba229c31c --- /dev/null +++ b/resources/qml/PrinterSelector/MachineSelectorButton.qml @@ -0,0 +1,110 @@ +// Copyright (c) 2018 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.7 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.3 + +import UM 1.1 as UM +import Cura 1.0 as Cura + +Button +{ + id: machineSelectorButton + + width: parent.width + height: UM.Theme.getSize("action_button").height + leftPadding: Math.round(1.5 * UM.Theme.getSize("default_margin").width) + checkable: true + + property var outputDevice: Cura.MachineManager.printerOutputDevices[0] + property var printerTypesList: [] + + function setPrinterTypesList() + { + printerTypesList = (checked && (outputDevice != null)) ? outputDevice.uniquePrinterTypes : [] + } + + contentItem: Item + { + width: machineSelectorButton.width - machineSelectorButton.leftPadding + height: UM.Theme.getSize("action_button").height + + Label + { + id: buttonText + anchors + { + left: parent.left + right: printerTypes.left + verticalCenter: parent.verticalCenter + } + text: machineSelectorButton.text + color: UM.Theme.getColor("text") + font: UM.Theme.getFont("action_button") + visible: text != "" + renderType: Text.NativeRendering + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + Row + { + id: printerTypes + width: childrenRect.width + + anchors + { + right: parent.right + verticalCenter: parent.verticalCenter + } + spacing: UM.Theme.getSize("narrow_margin").width + + Repeater + { + model: printerTypesList + delegate: Label + { + text: modelData + } + } + } + } + + background: Rectangle + { + id: backgroundRect + color: machineSelectorButton.hovered ? UM.Theme.getColor("action_button_hovered") : "transparent" + radius: UM.Theme.getSize("action_button_radius").width + border.width: UM.Theme.getSize("default_lining").width + border.color: machineSelectorButton.checked ? UM.Theme.getColor("primary") : "transparent" + } + + onClicked: + { + togglePopup() + Cura.MachineManager.setActiveMachine(model.id) + } + + MouseArea + { + id: mouseArea + anchors.fill: parent + onPressed: mouse.accepted = false + hoverEnabled: true + } + + Connections + { + target: outputDevice + onUniqueConfigurationsChanged: setPrinterTypesList() + } + + Connections + { + target: Cura.MachineManager + onOutputDevicesChanged: setPrinterTypesList() + } + + Component.onCompleted: setPrinterTypesList() +} diff --git a/resources/qml/qmldir b/resources/qml/qmldir index d5e4106d33..458338c78a 100644 --- a/resources/qml/qmldir +++ b/resources/qml/qmldir @@ -9,4 +9,5 @@ MaterialMenu 1.0 MaterialMenu.qml NozzleMenu 1.0 NozzleMenu.qml ActionPanelWidget 1.0 ActionPanelWidget.qml IconLabel 1.0 IconLabel.qml -OutputDevicesActionButton 1.0 OutputDevicesActionButton.qml \ No newline at end of file +OutputDevicesActionButton 1.0 OutputDevicesActionButton.qml +ExpandableComponent 1.0 ExpandableComponent.qml \ No newline at end of file From 0211122b1209d55db109cb00d8a442d30b4c970b Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Thu, 22 Nov 2018 14:46:13 +0100 Subject: [PATCH 056/146] Add a proper component with a label and a rectangle in the background that shows the type of printer. Contributes to CURA-5942. --- .../qml/PrinterSelector/MachineSelector.qml | 1 + .../PrinterSelector/MachineSelectorButton.qml | 24 ++++++++++++++++--- resources/qml/ViewOrientationControls.qml | 2 +- resources/themes/cura-light/theme.json | 5 +++- 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/resources/qml/PrinterSelector/MachineSelector.qml b/resources/qml/PrinterSelector/MachineSelector.qml index 9280c45cf4..66ed4d4a2c 100644 --- a/resources/qml/PrinterSelector/MachineSelector.qml +++ b/resources/qml/PrinterSelector/MachineSelector.qml @@ -84,6 +84,7 @@ Cura.ExpandableComponent { text: model.metadata["connect_group_name"] checked: Cura.MachineManager.activeMachineNetworkGroupName == model.metadata["connect_group_name"] + outputDevice: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null Connections { diff --git a/resources/qml/PrinterSelector/MachineSelectorButton.qml b/resources/qml/PrinterSelector/MachineSelectorButton.qml index 5ba229c31c..44b162d00d 100644 --- a/resources/qml/PrinterSelector/MachineSelectorButton.qml +++ b/resources/qml/PrinterSelector/MachineSelectorButton.qml @@ -17,7 +17,7 @@ Button leftPadding: Math.round(1.5 * UM.Theme.getSize("default_margin").width) checkable: true - property var outputDevice: Cura.MachineManager.printerOutputDevices[0] + property var outputDevice: null property var printerTypesList: [] function setPrinterTypesList() @@ -63,9 +63,27 @@ Button Repeater { model: printerTypesList - delegate: Label + delegate: Item { - text: modelData + width: UM.Theme.getSize("printer_type_label").width + height: UM.Theme.getSize("printer_type_label").height + + Rectangle + { + anchors.fill: parent + color: UM.Theme.getColor("printer_type_label_background") + } + + Label + { + id: printerTypeLabel + text: modelData + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + renderType: Text.NativeRendering + font: UM.Theme.getFont("very_small") + color: UM.Theme.getColor("text") + } } } } diff --git a/resources/qml/ViewOrientationControls.qml b/resources/qml/ViewOrientationControls.qml index acf75b1b48..fa5a51181d 100644 --- a/resources/qml/ViewOrientationControls.qml +++ b/resources/qml/ViewOrientationControls.qml @@ -21,7 +21,7 @@ Row { iconSource: UM.Theme.getIcon("view_3d") style: UM.Theme.styles.small_tool_button - onClicked:UM.Controller.rotateView("3d", 0) + onClicked: UM.Controller.rotateView("3d", 0) } // #2 Front view diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 2f86829f11..94a89342e7 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -81,7 +81,6 @@ "lining": [192, 193, 194, 255], "viewport_overlay": [0, 0, 0, 192], - "primary": [50, 130, 255, 255], "primary_hover": [48, 182, 231, 255], "primary_text": [255, 255, 255, 255], @@ -114,6 +113,8 @@ "toolbar_background": [255, 255, 255, 255], + "printer_type_label_background": [171, 171, 191, 255], + "text": [0, 0, 0, 255], "text_detail": [174, 174, 174, 128], "text_link": [50, 130, 255, 255], @@ -398,6 +399,8 @@ "views_selector": [0.0, 4.0], + "printer_type_label": [3.5, 1.5], + "default_radius": [0.25, 0.25], "wide_lining": [0.5, 0.5], From 5c30df2a688d858fb5b23e6ccc4250acae1dc9b6 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Thu, 22 Nov 2018 15:00:35 +0100 Subject: [PATCH 057/146] Create a reusable component for the printer type label. Other parts of the UI can just reuse it. Contributes to CURA-5942. --- resources/qml/ActionButton.qml | 1 - .../PrinterSelector/MachineSelectorButton.qml | 24 ++----------- .../qml/PrinterSelector/PrinterTypeLabel.qml | 34 +++++++++++++++++++ resources/qml/qmldir | 3 +- 4 files changed, 39 insertions(+), 23 deletions(-) create mode 100644 resources/qml/PrinterSelector/PrinterTypeLabel.qml diff --git a/resources/qml/ActionButton.qml b/resources/qml/ActionButton.qml index 2a8b894867..ff8ee4b149 100644 --- a/resources/qml/ActionButton.qml +++ b/resources/qml/ActionButton.qml @@ -3,7 +3,6 @@ import QtQuick 2.7 import QtQuick.Controls 2.1 -import QtQuick.Layouts 1.3 import UM 1.1 as UM diff --git a/resources/qml/PrinterSelector/MachineSelectorButton.qml b/resources/qml/PrinterSelector/MachineSelectorButton.qml index 44b162d00d..e98036dbc8 100644 --- a/resources/qml/PrinterSelector/MachineSelectorButton.qml +++ b/resources/qml/PrinterSelector/MachineSelectorButton.qml @@ -3,7 +3,6 @@ import QtQuick 2.7 import QtQuick.Controls 2.1 -import QtQuick.Layouts 1.3 import UM 1.1 as UM import Cura 1.0 as Cura @@ -15,6 +14,7 @@ Button width: parent.width height: UM.Theme.getSize("action_button").height leftPadding: Math.round(1.5 * UM.Theme.getSize("default_margin").width) + rightPadding: Math.round(1.5 * UM.Theme.getSize("default_margin").width) checkable: true property var outputDevice: null @@ -63,27 +63,9 @@ Button Repeater { model: printerTypesList - delegate: Item + delegate: Cura.PrinterTypeLabel { - width: UM.Theme.getSize("printer_type_label").width - height: UM.Theme.getSize("printer_type_label").height - - Rectangle - { - anchors.fill: parent - color: UM.Theme.getColor("printer_type_label_background") - } - - Label - { - id: printerTypeLabel - text: modelData - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - renderType: Text.NativeRendering - font: UM.Theme.getFont("very_small") - color: UM.Theme.getColor("text") - } + text: modelData } } } diff --git a/resources/qml/PrinterSelector/PrinterTypeLabel.qml b/resources/qml/PrinterSelector/PrinterTypeLabel.qml new file mode 100644 index 0000000000..cd9f3b9743 --- /dev/null +++ b/resources/qml/PrinterSelector/PrinterTypeLabel.qml @@ -0,0 +1,34 @@ +// Copyright (c) 2018 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.7 +import QtQuick.Controls 2.1 + +import UM 1.1 as UM + +// This component creates a label with the abbreviated name of a printer, with a rectangle surrounding the label. +// It is created in a separated place in order to be reused whenever needed. +Item +{ + property alias text: printerTypeLabel.text + + width: UM.Theme.getSize("printer_type_label").width + height: UM.Theme.getSize("printer_type_label").height + + Rectangle + { + anchors.fill: parent + color: UM.Theme.getColor("printer_type_label_background") + } + + Label + { + id: printerTypeLabel + text: "CFFFP" // As an abbreviated name of the Custom FFF Printer + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + renderType: Text.NativeRendering + font: UM.Theme.getFont("very_small") + color: UM.Theme.getColor("text") + } +} \ No newline at end of file diff --git a/resources/qml/qmldir b/resources/qml/qmldir index 458338c78a..67388100ca 100644 --- a/resources/qml/qmldir +++ b/resources/qml/qmldir @@ -10,4 +10,5 @@ NozzleMenu 1.0 NozzleMenu.qml ActionPanelWidget 1.0 ActionPanelWidget.qml IconLabel 1.0 IconLabel.qml OutputDevicesActionButton 1.0 OutputDevicesActionButton.qml -ExpandableComponent 1.0 ExpandableComponent.qml \ No newline at end of file +ExpandableComponent 1.0 ExpandableComponent.qml +PrinterTypeLabel 1.0 PrinterTypeLabel.qml \ No newline at end of file From 3f4d379908add0ec7054eb65e87ae91587ffaebd Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 22 Nov 2018 15:35:40 +0100 Subject: [PATCH 058/146] Added shadow to slice button CURA-5959 --- resources/qml/ActionButton.qml | 18 +++++++++++ .../qml/ActionPanel/SliceProcessWidget.qml | 31 +++++++++---------- resources/themes/cura-light/theme.json | 2 ++ 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/resources/qml/ActionButton.qml b/resources/qml/ActionButton.qml index 69d65e1c3f..8cd53b5d7e 100644 --- a/resources/qml/ActionButton.qml +++ b/resources/qml/ActionButton.qml @@ -5,6 +5,8 @@ import QtQuick 2.7 import QtQuick.Controls 2.1 import QtQuick.Layouts 1.3 +import QtGraphicalEffects 1.0 // For the dropshadow + import UM 1.1 as UM Button @@ -26,6 +28,9 @@ Button property color outlineHoverColor: hoverColor property color outlineDisabledColor: outlineColor + property alias shadowColor: shadow.color + property alias shadowEnabled: shadow.visible + // This property is used to indicate whether the button has a fixed width or the width would depend on the contents // Be careful when using fixedWidthMode, the translated texts can be too long that they won't fit. In any case, // we elide the text to the right so the text will be cut off with the three dots at the end. @@ -70,6 +75,19 @@ Button border.color: button.enabled ? (button.hovered ? button.outlineHoverColor : button.outlineColor) : button.outlineDisabledColor } + DropShadow + { + id: shadow + // Don't blur the shadow + radius: 0 + anchors.fill: backgroundRect + source: backgroundRect + verticalOffset: 2 + visible: false + // Should always be drawn behind the background. + z: backgroundRect.z - 1 + } + ToolTip { id: tooltip diff --git a/resources/qml/ActionPanel/SliceProcessWidget.qml b/resources/qml/ActionPanel/SliceProcessWidget.qml index 2d4a7b6b89..4f10e6879b 100644 --- a/resources/qml/ActionPanel/SliceProcessWidget.qml +++ b/resources/qml/ActionPanel/SliceProcessWidget.qml @@ -87,28 +87,27 @@ Column width: parent.width height: UM.Theme.getSize("action_panel_button").height fixedWidthMode: true - text: - { - if ([UM.Backend.NotStarted, UM.Backend.Error].indexOf(widget.backendState) != -1) - { - return catalog.i18nc("@button", "Slice") - } - if (autoSlice) - { - return catalog.i18nc("@button", "Auto slicing...") - } - return catalog.i18nc("@button", "Cancel") - } - enabled: !autoSlice && !disabledSlice // Get the current value from the preferences property bool autoSlice: UM.Preferences.getValue("general/auto_slice") // Disable the slice process when property bool disabledSlice: [UM.Backend.Done, UM.Backend.Error].indexOf(widget.backendState) != -1 - disabledColor: disabledSlice ? UM.Theme.getColor("action_button_disabled") : "transparent" - textDisabledColor: disabledSlice ? UM.Theme.getColor("action_button_disabled_text") : UM.Theme.getColor("primary") - outlineDisabledColor: disabledSlice ? UM.Theme.getColor("action_button_disabled_border") : "transparent" + text: + { + if ([UM.Backend.NotStarted, UM.Backend.Error].indexOf(widget.backendState) != -1) + { + return catalog.i18nc("@button", "Slice") + } + return catalog.i18nc("@button", "Cancel") + } + enabled: !autoSlice && !disabledSlice + visible: !autoSlice + + disabledColor: UM.Theme.getColor("action_button_disabled") + textDisabledColor: UM.Theme.getColor("action_button_disabled_text") + shadowEnabled: true + shadowColor: enabled ? UM.Theme.getColor("action_button_shadow"): UM.Theme.getColor("action_button_disabled_shadow") onClicked: sliceOrStopSlicing() } diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index fefc4adc14..748a4e2643 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -173,6 +173,8 @@ "action_button_disabled": [245, 245, 245, 255], "action_button_disabled_text": [127, 127, 127, 255], "action_button_disabled_border": [245, 245, 245, 255], + "action_button_shadow": [64, 47, 205, 255], + "action_button_disabled_shadow": [228, 228, 228, 255], "print_button_ready": [50, 130, 255, 255], "print_button_ready_border": [50, 130, 255, 255], From 692868a0b4f25ba4aa8f2a1fedfe05fe36c9f0c9 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Thu, 22 Nov 2018 15:45:38 +0100 Subject: [PATCH 059/146] Create a function that given a printer type name, it will return and abbreviated name. Contributes to CURA-5942. --- cura/PrintInformation.py | 16 +------------- cura/Settings/MachineManager.py | 21 +++++++++++++++++++ .../PrinterSelector/MachineSelectorButton.qml | 2 +- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/cura/PrintInformation.py b/cura/PrintInformation.py index e11f70a54c..f1d8e81b3a 100644 --- a/cura/PrintInformation.py +++ b/cura/PrintInformation.py @@ -395,21 +395,7 @@ class PrintInformation(QObject): return active_machine_type_name = global_container_stack.definition.getName() - abbr_machine = "" - for word in re.findall(r"[\w']+", active_machine_type_name): - if word.lower() == "ultimaker": - abbr_machine += "UM" - elif word.isdigit(): - abbr_machine += word - else: - stripped_word = self._stripAccents(word.upper()) - # - use only the first character if the word is too long (> 3 characters) - # - use the whole word if it's not too long (<= 3 characters) - if len(stripped_word) > 3: - stripped_word = stripped_word[0] - abbr_machine += stripped_word - - self._abbr_machine = abbr_machine + self._abbr_machine = self._application.getMachineManager().getAbbreviatedMachineName(active_machine_type_name) ## Utility method that strips accents from characters (eg: â -> a) def _stripAccents(self, to_strip: str) -> str: diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index f321ce94a6..a65d2ed302 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -3,6 +3,8 @@ import collections import time +import re +import unicodedata from typing import Any, Callable, List, Dict, TYPE_CHECKING, Optional, cast from UM.ConfigurationErrorMessage import ConfigurationErrorMessage @@ -1537,3 +1539,22 @@ class MachineManager(QObject): with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): self.updateMaterialWithVariant(None) self._updateQualityWithMaterial() + + ## This function will translate any printer type name to an abbreviated printer type name + @pyqtSlot(str, result = str) + def getAbbreviatedMachineName(self, machine_type_name: str) -> str: + abbr_machine = "" + for word in re.findall(r"[\w']+", machine_type_name): + if word.lower() == "ultimaker": + abbr_machine += "UM" + elif word.isdigit(): + abbr_machine += word + else: + stripped_word = ''.join(char for char in unicodedata.normalize('NFD', word.upper()) if unicodedata.category(char) != 'Mn') + # - use only the first character if the word is too long (> 3 characters) + # - use the whole word if it's not too long (<= 3 characters) + if len(stripped_word) > 3: + stripped_word = stripped_word[0] + abbr_machine += stripped_word + + return abbr_machine diff --git a/resources/qml/PrinterSelector/MachineSelectorButton.qml b/resources/qml/PrinterSelector/MachineSelectorButton.qml index e98036dbc8..e7b44a4447 100644 --- a/resources/qml/PrinterSelector/MachineSelectorButton.qml +++ b/resources/qml/PrinterSelector/MachineSelectorButton.qml @@ -65,7 +65,7 @@ Button model: printerTypesList delegate: Cura.PrinterTypeLabel { - text: modelData + text: Cura.MachineManager.getAbbreviatedMachineName(modelData) } } } From 06cb628699bf23e04c5ab6e06b462582053f64c3 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 22 Nov 2018 15:46:56 +0100 Subject: [PATCH 060/146] Update desktop and mimeinfo to add gcode mime type CURA-5878 --- cura.desktop.in | 2 +- cura.sharedmimeinfo | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/cura.desktop.in b/cura.desktop.in index fbe8b30fed..b0195015a5 100644 --- a/cura.desktop.in +++ b/cura.desktop.in @@ -13,6 +13,6 @@ TryExec=@CMAKE_INSTALL_FULL_BINDIR@/cura Icon=cura-icon Terminal=false Type=Application -MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml; +MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml;text/x-gcode; Categories=Graphics; Keywords=3D;Printing;Slicer; diff --git a/cura.sharedmimeinfo b/cura.sharedmimeinfo index 23d38795eb..ed9099d425 100644 --- a/cura.sharedmimeinfo +++ b/cura.sharedmimeinfo @@ -19,4 +19,12 @@ + + + Gcode file + + + + + \ No newline at end of file From 1d84bd735660cc8c8b98157854fc15b46d5cd55e Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Thu, 22 Nov 2018 17:37:10 +0100 Subject: [PATCH 061/146] Improve the machine selector header to show an icon in case it is a network printer. Contributes to CURA-5942. --- .../qml/ActionPanel/OutputProcessWidget.qml | 2 ++ .../qml/ActionPanel/SliceProcessWidget.qml | 1 + resources/qml/IconLabel.qml | 13 ++++++----- .../qml/PrinterSelector/MachineSelector.qml | 22 ++++++++++++++----- ...us_connected.svg => printer_connected.svg} | 0 .../themes/cura-light/icons/printer_group.svg | 22 +++++++++++++------ .../cura-light/icons/printer_single.svg | 15 ++++++------- .../cura-light/icons/tab_status_busy.svg | 13 ----------- .../cura-light/icons/tab_status_finished.svg | 13 ----------- .../cura-light/icons/tab_status_paused.svg | 13 ----------- .../cura-light/icons/tab_status_stopped.svg | 13 ----------- .../cura-light/icons/tab_status_unknown.svg | 13 ----------- resources/themes/cura-light/theme.json | 1 + 13 files changed, 50 insertions(+), 91 deletions(-) rename resources/themes/cura-light/icons/{tab_status_connected.svg => printer_connected.svg} (100%) delete mode 100644 resources/themes/cura-light/icons/tab_status_busy.svg delete mode 100644 resources/themes/cura-light/icons/tab_status_finished.svg delete mode 100644 resources/themes/cura-light/icons/tab_status_paused.svg delete mode 100644 resources/themes/cura-light/icons/tab_status_stopped.svg delete mode 100644 resources/themes/cura-light/icons/tab_status_unknown.svg diff --git a/resources/qml/ActionPanel/OutputProcessWidget.qml b/resources/qml/ActionPanel/OutputProcessWidget.qml index 3c4386f079..87f9d2015d 100644 --- a/resources/qml/ActionPanel/OutputProcessWidget.qml +++ b/resources/qml/ActionPanel/OutputProcessWidget.qml @@ -47,6 +47,7 @@ Column { id: estimatedTime width: parent.width + height: childrenRect.height text: PrintInformation.currentPrintTime.getDisplayString(UM.DurationFormat.Long) source: UM.Theme.getIcon("clock") @@ -57,6 +58,7 @@ Column { id: estimatedCosts width: parent.width + height: childrenRect.height property var printMaterialLengths: PrintInformation.materialLengths property var printMaterialWeights: PrintInformation.materialWeights diff --git a/resources/qml/ActionPanel/SliceProcessWidget.qml b/resources/qml/ActionPanel/SliceProcessWidget.qml index 2d4a7b6b89..9c46539220 100644 --- a/resources/qml/ActionPanel/SliceProcessWidget.qml +++ b/resources/qml/ActionPanel/SliceProcessWidget.qml @@ -44,6 +44,7 @@ Column { id: message width: parent.width + height: childrenRect.height visible: widget.backendState == UM.Backend.Error text: catalog.i18nc("@label:PrintjobStatus", "Unable to Slice") diff --git a/resources/qml/IconLabel.qml b/resources/qml/IconLabel.qml index 7c90382892..90930e91c7 100644 --- a/resources/qml/IconLabel.qml +++ b/resources/qml/IconLabel.qml @@ -16,32 +16,35 @@ Item property alias source: icon.source property alias color: label.color property alias font: label.font - - height: childrenRect.height + property alias iconSize: icon.width UM.RecolorImage { id: icon anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter - source: UM.Theme.getIcon("dot") + source: "" width: UM.Theme.getSize("section_icon").width - height: UM.Theme.getSize("section_icon").height + height: width sourceSize.width: width sourceSize.height: height color: label.color + visible: source != "" } Label { id: label - anchors.left: icon.right + anchors.left: icon.visible ? icon.right : parent.left + anchors.right: parent.right anchors.leftMargin: UM.Theme.getSize("thin_margin").width anchors.verticalCenter: icon.verticalCenter text: "Empty label" + elide: Text.ElideRight color: UM.Theme.getColor("text") font: UM.Theme.getFont("very_small") renderType: Text.NativeRendering diff --git a/resources/qml/PrinterSelector/MachineSelector.qml b/resources/qml/PrinterSelector/MachineSelector.qml index 66ed4d4a2c..d10478227a 100644 --- a/resources/qml/PrinterSelector/MachineSelector.qml +++ b/resources/qml/PrinterSelector/MachineSelector.qml @@ -14,6 +14,7 @@ Cura.ExpandableComponent id: machineSelector property bool isNetworkPrinter: Cura.MachineManager.activeMachineNetworkKey != "" + property var outputDevice: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null popupPadding: 0 popupAlignment: Cura.ExpandableComponent.PopupAlignment.AlignLeft @@ -25,15 +26,24 @@ Cura.ExpandableComponent name: "cura" } - headerItem: Label + headerItem: Cura.IconLabel { text: isNetworkPrinter ? Cura.MachineManager.activeMachineNetworkGroupName : Cura.MachineManager.activeMachineName - verticalAlignment: Text.AlignVCenter - height: parent.height - elide: Text.ElideRight - renderType: Text.NativeRendering - font: UM.Theme.getFont("default") + source: + { + if (isNetworkPrinter && machineSelector.outputDevice != null) + { + if (machineSelector.outputDevice.clusterSize > 1) + { + return UM.Theme.getIcon("printer_group") + } + return UM.Theme.getIcon("printer_single") + } + return "" + } + font: UM.Theme.getFont("medium") color: UM.Theme.getColor("text") + iconSize: UM.Theme.getSize("machine_selector_icon").width } popupItem: Item diff --git a/resources/themes/cura-light/icons/tab_status_connected.svg b/resources/themes/cura-light/icons/printer_connected.svg similarity index 100% rename from resources/themes/cura-light/icons/tab_status_connected.svg rename to resources/themes/cura-light/icons/printer_connected.svg diff --git a/resources/themes/cura-light/icons/printer_group.svg b/resources/themes/cura-light/icons/printer_group.svg index 614bea90b8..5e439faca4 100644 --- a/resources/themes/cura-light/icons/printer_group.svg +++ b/resources/themes/cura-light/icons/printer_group.svg @@ -1,12 +1,20 @@ - - - icn_groupPrinters + + + Icon/ group printer/ disconnected Created with Sketch. - - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/themes/cura-light/icons/printer_single.svg b/resources/themes/cura-light/icons/printer_single.svg index f7dc83987d..69c4e212bc 100644 --- a/resources/themes/cura-light/icons/printer_single.svg +++ b/resources/themes/cura-light/icons/printer_single.svg @@ -1,13 +1,12 @@ - - - icn_singlePrinter + + + Icon/ single printer/ disconnected Created with Sketch. - - - - - + + + + diff --git a/resources/themes/cura-light/icons/tab_status_busy.svg b/resources/themes/cura-light/icons/tab_status_busy.svg deleted file mode 100644 index debe4f6360..0000000000 --- a/resources/themes/cura-light/icons/tab_status_busy.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - Busy - Created with Sketch. - - - - - - - - \ No newline at end of file diff --git a/resources/themes/cura-light/icons/tab_status_finished.svg b/resources/themes/cura-light/icons/tab_status_finished.svg deleted file mode 100644 index 2519f2f862..0000000000 --- a/resources/themes/cura-light/icons/tab_status_finished.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - Wait cleanup - Created with Sketch. - - - - - - - - \ No newline at end of file diff --git a/resources/themes/cura-light/icons/tab_status_paused.svg b/resources/themes/cura-light/icons/tab_status_paused.svg deleted file mode 100644 index bab6c9ca6b..0000000000 --- a/resources/themes/cura-light/icons/tab_status_paused.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - paused - Created with Sketch. - - - - - - - - \ No newline at end of file diff --git a/resources/themes/cura-light/icons/tab_status_stopped.svg b/resources/themes/cura-light/icons/tab_status_stopped.svg deleted file mode 100644 index c9b150db3a..0000000000 --- a/resources/themes/cura-light/icons/tab_status_stopped.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - Aborted - Created with Sketch. - - - - - - - - \ No newline at end of file diff --git a/resources/themes/cura-light/icons/tab_status_unknown.svg b/resources/themes/cura-light/icons/tab_status_unknown.svg deleted file mode 100644 index 9f413baffc..0000000000 --- a/resources/themes/cura-light/icons/tab_status_unknown.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - Unknown - Created with Sketch. - - - - - - - - \ No newline at end of file diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 61b7600384..8f110db65d 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -396,6 +396,7 @@ "machine_selector_widget": [20.0, 4.0], "machine_selector_widget_content": [25.0, 32.0], + "machine_selector_icon": [2.66, 2.66], "views_selector": [16.0, 4.5], From bb5c0326de589ee09b2398b74a542f838d6ea8e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijn=20De=C3=A9?= Date: Fri, 23 Nov 2018 09:20:19 +0100 Subject: [PATCH 062/146] Used duoble quotes iso single quotes --- plugins/UM3NetworkPrinting/src/Models.py | 52 ++++++++++++------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Models.py b/plugins/UM3NetworkPrinting/src/Models.py index d5e1007555..bcdeb8299c 100644 --- a/plugins/UM3NetworkPrinting/src/Models.py +++ b/plugins/UM3NetworkPrinting/src/Models.py @@ -2,32 +2,32 @@ # Cura is released under the terms of the LGPLv3 or higher. from collections import namedtuple -ClusterMaterial = namedtuple('ClusterMaterial', [ - 'guid', # Type: str - 'material', # Type: str - 'brand', # Type: str - 'version', # Type: int - 'color', # Type: str - 'density' # Type: str +ClusterMaterial = namedtuple("ClusterMaterial", [ + "guid", # Type: str + "material", # Type: str + "brand", # Type: str + "version", # Type: int + "color", # Type: str + "density" # Type: str ]) -LocalMaterial = namedtuple('LocalMaterial', [ - 'GUID', # Type: str - 'id', # Type: str - 'type', # Type: str - 'status', # Type: str - 'base_file', # Type: str - 'setting_version', # Type: int - 'version', # Type: int - 'name', # Type: str - 'brand', # Type: str - 'material', # Type: str - 'color_name', # Type: str - 'color_code', # Type: str - 'description', # Type: str - 'adhesion_info', # Type: str - 'approximate_diameter', # Type: str - 'properties', # Type: str - 'definition', # Type: str - 'compatible' # Type: str +LocalMaterial = namedtuple("LocalMaterial", [ + "GUID", # Type: str + "id", # Type: str + "type", # Type: str + "status", # Type: str + "base_file", # Type: str + "setting_version", # Type: int + "version", # Type: int + "name", # Type: str + "brand", # Type: str + "material", # Type: str + "color_name", # Type: str + "color_code", # Type: str + "description", # Type: str + "adhesion_info", # Type: str + "approximate_diameter", # Type: str + "properties", # Type: str + "definition", # Type: str + "compatible" # Type: str ]) From 294527f7febf96d81f4bcdf62b142a552d58fbbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijn=20De=C3=A9?= Date: Fri, 23 Nov 2018 09:21:09 +0100 Subject: [PATCH 063/146] Review changes --- .../UM3NetworkPrinting/src/SendMaterialJob.py | 19 ++++++------ .../tests/TestSendMaterialJob.py | 29 +++++++++++++------ 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py index 72269040e7..48760af28e 100644 --- a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py @@ -7,6 +7,7 @@ from typing import Dict, TYPE_CHECKING, Set from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest +from UM.Application import Application from UM.Job import Job from UM.Logger import Logger from UM.MimeTypeDatabase import MimeTypeDatabase @@ -27,7 +28,6 @@ class SendMaterialJob(Job): def __init__(self, device: "ClusterUM3OutputDevice") -> None: super().__init__() self.device = device # type: ClusterUM3OutputDevice - self._application = CuraApplication.getInstance() # type: CuraApplication ## Send the request to the printer and register a callback def run(self) -> None: @@ -44,12 +44,9 @@ class SendMaterialJob(Job): return # Collect materials from the printer's reply and send the missing ones if needed. - try: - remote_materials_by_guid = self._parseReply(reply) - if remote_materials_by_guid: - self._sendMissingMaterials(remote_materials_by_guid) - except TypeError: - Logger.logException("w", "Error parsing materials from printer") + remote_materials_by_guid = self._parseReply(reply) + if remote_materials_by_guid: + self._sendMissingMaterials(remote_materials_by_guid) ## Determine which materials should be updated and send them to the printer. # @@ -158,8 +155,12 @@ class SendMaterialJob(Job): try: remote_materials = json.loads(reply.readAll().data().decode("utf-8")) return {material["guid"]: ClusterMaterial(**material) for material in remote_materials} + except UnicodeDecodeError: + Logger.log("e", "Request material storage on printer: I didn't understand the printer's answer.") except json.JSONDecodeError: - Logger.logException("w", "Error parsing materials from printer") + Logger.log("e", "Request material storage on printer: I didn't understand the printer's answer.") + except TypeError: + Logger.log("e", "Request material storage on printer: Printer's answer was missing GUIDs.") ## Retrieves a list of local materials # @@ -168,7 +169,7 @@ class SendMaterialJob(Job): # \return a dictionary of LocalMaterial objects by GUID def _getLocalMaterials(self) -> Dict[str, LocalMaterial]: result = {} # type: Dict[str, LocalMaterial] - container_registry = self._application.getContainerRegistry() + container_registry = Application.getInstance().getContainerRegistry() material_containers = container_registry.findContainersMetadata(type = "material") # Find the latest version of all material containers in the registry. diff --git a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py index 548704fd33..f5a475b3ab 100644 --- a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py @@ -8,7 +8,7 @@ from unittest.mock import patch, call from PyQt5.QtCore import QByteArray from UM.MimeTypeDatabase import MimeType -from cura.CuraApplication import CuraApplication +from UM.Application import Application from plugins.UM3NetworkPrinting.src.SendMaterialJob import SendMaterialJob @@ -70,6 +70,17 @@ class TestSendMaterialJob(TestCase): self.assertEqual([call.attribute(0), call.errorString()], reply_mock.method_calls) self.assertEqual(0, device_mock.createFormPart.call_count) + def test__onGetRemoteMaterials_withWrongEncoding(self, reply_mock, device_mock): + reply_mock.attribute.return_value = 200 + reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("cp500")) + job = SendMaterialJob(device_mock) + job._onGetRemoteMaterials(reply_mock) + + # We expect the reply to be called once to try to get the printers from the list (readAll()). + # Given that the parsing fails we do no expect the device to be called for any follow up. + self.assertEqual([call.attribute(0), call.readAll()], reply_mock.method_calls) + self.assertEqual(0, device_mock.createFormPart.call_count) + def test__onGetRemoteMaterials_withBadJsonAnswer(self, reply_mock, device_mock): reply_mock.attribute.return_value = 200 reply_mock.readAll.return_value = QByteArray(b"Six sick hicks nick six slick bricks with picks and sticks.") @@ -95,7 +106,7 @@ class TestSendMaterialJob(TestCase): self.assertEqual(0, device_mock.createFormPart.call_count) @patch("cura.Settings.CuraContainerRegistry") - @patch("cura.CuraApplication") + @patch("UM.Application") def test__onGetRemoteMaterials_withInvalidVersionInLocalMaterial(self, application_mock, container_registry_mock, reply_mock, device_mock): reply_mock.attribute.return_value = 200 @@ -107,7 +118,7 @@ class TestSendMaterialJob(TestCase): application_mock.getContainerRegistry.return_value = container_registry_mock - with mock.patch.object(CuraApplication, "getInstance", new = lambda: application_mock): + with mock.patch.object(Application, "getInstance", new = lambda: application_mock): job = SendMaterialJob(device_mock) job._onGetRemoteMaterials(reply_mock) @@ -117,7 +128,7 @@ class TestSendMaterialJob(TestCase): self.assertEqual(0, device_mock.createFormPart.call_count) @patch("cura.Settings.CuraContainerRegistry") - @patch("cura.CuraApplication") + @patch("UM.Application") def test__onGetRemoteMaterials_withNoUpdate(self, application_mock, container_registry_mock, reply_mock, device_mock): application_mock.getContainerRegistry.return_value = container_registry_mock @@ -129,7 +140,7 @@ class TestSendMaterialJob(TestCase): reply_mock.attribute.return_value = 200 reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii")) - with mock.patch.object(CuraApplication, "getInstance", new = lambda: application_mock): + with mock.patch.object(Application, "getInstance", new = lambda: application_mock): job = SendMaterialJob(device_mock) job._onGetRemoteMaterials(reply_mock) @@ -140,7 +151,7 @@ class TestSendMaterialJob(TestCase): self.assertEqual(0, device_mock.postFormWithParts.call_count) @patch("cura.Settings.CuraContainerRegistry") - @patch("cura.CuraApplication") + @patch("UM.Application") def test__onGetRemoteMaterials_withUpdatedMaterial(self, application_mock, container_registry_mock, reply_mock, device_mock): application_mock.getContainerRegistry.return_value = container_registry_mock @@ -154,7 +165,7 @@ class TestSendMaterialJob(TestCase): reply_mock.attribute.return_value = 200 reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii")) - with mock.patch.object(CuraApplication, "getInstance", new = lambda: application_mock): + with mock.patch.object(Application, "getInstance", new = lambda: application_mock): job = SendMaterialJob(device_mock) job._onGetRemoteMaterials(reply_mock) @@ -169,7 +180,7 @@ class TestSendMaterialJob(TestCase): device_mock.method_calls) @patch("cura.Settings.CuraContainerRegistry") - @patch("cura.CuraApplication") + @patch("UM.Application") def test__onGetRemoteMaterials_withNewMaterial(self, application_mock, container_registry_mock, reply_mock, device_mock): application_mock.getContainerRegistry.return_value = container_registry_mock @@ -182,7 +193,7 @@ class TestSendMaterialJob(TestCase): reply_mock.attribute.return_value = 200 reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_BLACK]).encode("ascii")) - with mock.patch.object(CuraApplication, "getInstance", new = lambda: application_mock): + with mock.patch.object(Application, "getInstance", new = lambda: application_mock): job = SendMaterialJob(device_mock) job._onGetRemoteMaterials(reply_mock) From 76542a82d5a19b3dd9ea2a3f2c785d8fd956fc57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijn=20De=C3=A9?= Date: Fri, 23 Nov 2018 10:33:57 +0100 Subject: [PATCH 064/146] Removed the asserts on internals --- .../tests/TestSendMaterialJob.py | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py index f5a475b3ab..b669eb192a 100644 --- a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py @@ -1,4 +1,5 @@ # Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import io import json @@ -66,8 +67,7 @@ class TestSendMaterialJob(TestCase): job = SendMaterialJob(device_mock) job._onGetRemoteMaterials(reply_mock) - # We expect the error string to be retrieved and the device not to be called for any follow up. - self.assertEqual([call.attribute(0), call.errorString()], reply_mock.method_calls) + # We expect the device not to be called for any follow up. self.assertEqual(0, device_mock.createFormPart.call_count) def test__onGetRemoteMaterials_withWrongEncoding(self, reply_mock, device_mock): @@ -76,9 +76,7 @@ class TestSendMaterialJob(TestCase): job = SendMaterialJob(device_mock) job._onGetRemoteMaterials(reply_mock) - # We expect the reply to be called once to try to get the printers from the list (readAll()). # Given that the parsing fails we do no expect the device to be called for any follow up. - self.assertEqual([call.attribute(0), call.readAll()], reply_mock.method_calls) self.assertEqual(0, device_mock.createFormPart.call_count) def test__onGetRemoteMaterials_withBadJsonAnswer(self, reply_mock, device_mock): @@ -87,9 +85,7 @@ class TestSendMaterialJob(TestCase): job = SendMaterialJob(device_mock) job._onGetRemoteMaterials(reply_mock) - # We expect the reply to be called once to try to get the printers from the list (readAll()). # Given that the parsing fails we do no expect the device to be called for any follow up. - self.assertEqual([call.attribute(0), call.readAll()], reply_mock.method_calls) self.assertEqual(0, device_mock.createFormPart.call_count) def test__onGetRemoteMaterials_withMissingGuidInRemoteMaterial(self, reply_mock, device_mock): @@ -100,9 +96,7 @@ class TestSendMaterialJob(TestCase): job = SendMaterialJob(device_mock) job._onGetRemoteMaterials(reply_mock) - # We expect the reply to be called once to try to get the printers from the list (readAll()). # Given that parsing fails we do not expect the device to be called for any follow up. - self.assertEqual([call.attribute(0), call.readAll()], reply_mock.method_calls) self.assertEqual(0, device_mock.createFormPart.call_count) @patch("cura.Settings.CuraContainerRegistry") @@ -122,9 +116,6 @@ class TestSendMaterialJob(TestCase): job = SendMaterialJob(device_mock) job._onGetRemoteMaterials(reply_mock) - self.assertEqual([call.attribute(0), call.readAll()], reply_mock.method_calls) - self.assertEqual([call.getContainerRegistry()], application_mock.method_calls) - self.assertEqual([call.findContainersMetadata(type = "material")], container_registry_mock.method_calls) self.assertEqual(0, device_mock.createFormPart.call_count) @patch("cura.Settings.CuraContainerRegistry") @@ -144,9 +135,6 @@ class TestSendMaterialJob(TestCase): job = SendMaterialJob(device_mock) job._onGetRemoteMaterials(reply_mock) - self.assertEqual([call.attribute(0), call.readAll()], reply_mock.method_calls) - self.assertEqual([call.getContainerRegistry()], application_mock.method_calls) - self.assertEqual([call.findContainersMetadata(type = "material")], container_registry_mock.method_calls) self.assertEqual(0, device_mock.createFormPart.call_count) self.assertEqual(0, device_mock.postFormWithParts.call_count) @@ -169,9 +157,6 @@ class TestSendMaterialJob(TestCase): job = SendMaterialJob(device_mock) job._onGetRemoteMaterials(reply_mock) - self.assertEqual([call.attribute(0), call.readAll()], reply_mock.method_calls) - self.assertEqual([call.getContainerRegistry()], application_mock.method_calls) - self.assertEqual([call.findContainersMetadata(type = "material")], container_registry_mock.method_calls) self.assertEqual(1, device_mock.createFormPart.call_count) self.assertEqual(1, device_mock.postFormWithParts.call_count) self.assertEquals( @@ -197,9 +182,6 @@ class TestSendMaterialJob(TestCase): job = SendMaterialJob(device_mock) job._onGetRemoteMaterials(reply_mock) - self.assertEqual([call.attribute(0), call.readAll()], reply_mock.method_calls) - self.assertEqual([call.getContainerRegistry()], application_mock.method_calls) - self.assertEqual([call.findContainersMetadata(type = "material")], container_registry_mock.method_calls) self.assertEqual(1, device_mock.createFormPart.call_count) self.assertEqual(1, device_mock.postFormWithParts.call_count) self.assertEquals( From 67dc415b58eec45b497b3cd29eedee538514ec0a Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Fri, 23 Nov 2018 10:46:55 +0100 Subject: [PATCH 065/146] Add the connected icon to the printer selector that shows a blue icon when the printer is connected. If not connected then there is no icon. Contributes to CURA-5942. --- .../qml/PrinterSelector/MachineSelector.qml | 36 +++++++++++++++++-- .../themes/cura-light/icons/connected.svg | 5 --- .../themes/cura-light/icons/disconnected.svg | 6 ---- resources/themes/cura-light/theme.json | 2 +- 4 files changed, 35 insertions(+), 14 deletions(-) delete mode 100644 resources/themes/cura-light/icons/connected.svg delete mode 100644 resources/themes/cura-light/icons/disconnected.svg diff --git a/resources/qml/PrinterSelector/MachineSelector.qml b/resources/qml/PrinterSelector/MachineSelector.qml index d10478227a..a33c54ed88 100644 --- a/resources/qml/PrinterSelector/MachineSelector.qml +++ b/resources/qml/PrinterSelector/MachineSelector.qml @@ -14,6 +14,7 @@ Cura.ExpandableComponent id: machineSelector property bool isNetworkPrinter: Cura.MachineManager.activeMachineNetworkKey != "" + property bool isPrinterConnected: Cura.MachineManager.printerConnected property var outputDevice: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null popupPadding: 0 @@ -31,9 +32,9 @@ Cura.ExpandableComponent text: isNetworkPrinter ? Cura.MachineManager.activeMachineNetworkGroupName : Cura.MachineManager.activeMachineName source: { - if (isNetworkPrinter && machineSelector.outputDevice != null) + if (isNetworkPrinter) { - if (machineSelector.outputDevice.clusterSize > 1) + if (machineSelector.outputDevice != null && machineSelector.outputDevice.clusterSize > 1) { return UM.Theme.getIcon("printer_group") } @@ -44,6 +45,37 @@ Cura.ExpandableComponent font: UM.Theme.getFont("medium") color: UM.Theme.getColor("text") iconSize: UM.Theme.getSize("machine_selector_icon").width + + UM.RecolorImage + { + id: icon + + anchors.bottom: parent.bottom + x: UM.Theme.getSize("thick_margin").width + + source: UM.Theme.getIcon("printer_connected") + width: UM.Theme.getSize("printer_status_icon").width + height: UM.Theme.getSize("printer_status_icon").height + + sourceSize.width: width + sourceSize.height: height + + color: UM.Theme.getColor("primary") + visible: isNetworkPrinter && isPrinterConnected + + // Make a themable circle in the background so we can change it in other themes + Rectangle + { + id: iconBackground + anchors.centerIn: parent + // Make it a bit bigger so there is an outline + width: parent.width + 2 + height: parent.height + 2 + radius: Math.round(width / 2) + color: UM.Theme.getColor("main_background") + z: parent.z - 1 + } + } } popupItem: Item diff --git a/resources/themes/cura-light/icons/connected.svg b/resources/themes/cura-light/icons/connected.svg deleted file mode 100644 index 18423bb6c4..0000000000 --- a/resources/themes/cura-light/icons/connected.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/resources/themes/cura-light/icons/disconnected.svg b/resources/themes/cura-light/icons/disconnected.svg deleted file mode 100644 index 019dff117e..0000000000 --- a/resources/themes/cura-light/icons/disconnected.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 8f110db65d..23b1ffcada 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -449,7 +449,7 @@ "favorites_button": [2, 2], "favorites_button_icon": [1.2, 1.2], - "printer_status_icon": [1.8, 1.8], + "printer_status_icon": [1.0, 1.0], "printer_sync_icon": [1.2, 1.2], "button_tooltip": [1.0, 1.3], From 48c24b103484ddf40a1e3da13ac480a983a4f493 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Fri, 23 Nov 2018 10:49:50 +0100 Subject: [PATCH 066/146] Remove log entry for bonjour services added For most networks that would not be a problematic log entry. But for our own debugging on Ultimaker's network this is a very spammy log entry and doesn't add much value anyway. --- plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index 9c070f2de2..daea696cd1 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin @@ -325,13 +325,12 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): ## Handler for zeroConf detection. # Return True or False indicating if the process succeeded. - # Note that this function can take over 3 seconds to complete. Be carefull calling it from the main thread. + # Note that this function can take over 3 seconds to complete. Be careful + # calling it from the main thread. def _onServiceChanged(self, zero_conf, service_type, name, state_change): if state_change == ServiceStateChange.Added: - Logger.log("d", "Bonjour service added: %s" % name) - # First try getting info from zero-conf cache - info = ServiceInfo(service_type, name, properties={}) + info = ServiceInfo(service_type, name, properties = {}) for record in zero_conf.cache.entries_with_name(name.lower()): info.update_record(zero_conf, time(), record) From 3ba4b9fd81ac3b38aa393d71697f9e1514ecb6d7 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 23 Nov 2018 11:20:53 +0100 Subject: [PATCH 067/146] Add shadows to various used actionbuttons CURA-5959 --- .../ActionPanel/OutputDevicesActionButton.qml | 6 +++++- .../qml/ActionPanel/OutputProcessWidget.qml | 3 +++ .../qml/ActionPanel/SliceProcessWidget.qml | 17 ++++++++--------- resources/themes/cura-light/theme.json | 2 ++ 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/resources/qml/ActionPanel/OutputDevicesActionButton.qml b/resources/qml/ActionPanel/OutputDevicesActionButton.qml index be79a1893e..9682dddf14 100644 --- a/resources/qml/ActionPanel/OutputDevicesActionButton.qml +++ b/resources/qml/ActionPanel/OutputDevicesActionButton.qml @@ -17,7 +17,8 @@ Item id: saveToButton height: parent.height fixedWidthMode: true - + shadowEnabled: true + shadowColor: UM.Theme.getColor("primary_shadow") anchors { top: parent.top @@ -42,6 +43,9 @@ Item id: deviceSelectionMenu height: parent.height + shadowEnabled: true + shadowColor: UM.Theme.getColor("primary_shadow") + anchors { top: parent.top diff --git a/resources/qml/ActionPanel/OutputProcessWidget.qml b/resources/qml/ActionPanel/OutputProcessWidget.qml index 3c4386f079..79b9898e49 100644 --- a/resources/qml/ActionPanel/OutputProcessWidget.qml +++ b/resources/qml/ActionPanel/OutputProcessWidget.qml @@ -115,6 +115,9 @@ Column textHoverColor: UM.Theme.getColor("text") onClicked: UM.Controller.setActiveStage("PreviewStage") visible: UM.Controller.activeStage != null && UM.Controller.activeStage.stageId != "PreviewStage" + + shadowEnabled: true + shadowColor: UM.Theme.getColor("action_button_disabled_shadow") } Cura.OutputDevicesActionButton diff --git a/resources/qml/ActionPanel/SliceProcessWidget.qml b/resources/qml/ActionPanel/SliceProcessWidget.qml index 4f10e6879b..7cce323905 100644 --- a/resources/qml/ActionPanel/SliceProcessWidget.qml +++ b/resources/qml/ActionPanel/SliceProcessWidget.qml @@ -93,21 +93,20 @@ Column // Disable the slice process when property bool disabledSlice: [UM.Backend.Done, UM.Backend.Error].indexOf(widget.backendState) != -1 - text: - { - if ([UM.Backend.NotStarted, UM.Backend.Error].indexOf(widget.backendState) != -1) - { - return catalog.i18nc("@button", "Slice") - } - return catalog.i18nc("@button", "Cancel") - } + property bool isSlicing: [UM.Backend.NotStarted, UM.Backend.Error].indexOf(widget.backendState) == -1 + + text: isSlicing ? catalog.i18nc("@button", "Cancel") : catalog.i18nc("@button", "Slice") + enabled: !autoSlice && !disabledSlice visible: !autoSlice + color: isSlicing ? UM.Theme.getColor("secondary"): UM.Theme.getColor("primary") + textColor: isSlicing ? UM.Theme.getColor("primary"): UM.Theme.getColor("button_text") + disabledColor: UM.Theme.getColor("action_button_disabled") textDisabledColor: UM.Theme.getColor("action_button_disabled_text") shadowEnabled: true - shadowColor: enabled ? UM.Theme.getColor("action_button_shadow"): UM.Theme.getColor("action_button_disabled_shadow") + shadowColor: isSlicing ? UM.Theme.getColor("secondary_shadow") : enabled ? UM.Theme.getColor("action_button_shadow"): UM.Theme.getColor("action_button_disabled_shadow") onClicked: sliceOrStopSlicing() } diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 748a4e2643..c5802b6a7e 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -83,10 +83,12 @@ "primary": [50, 130, 255, 255], + "primary_shadow": [64, 47, 205, 255], "primary_hover": [48, 182, 231, 255], "primary_text": [255, 255, 255, 255], "border": [127, 127, 127, 255], "secondary": [245, 245, 245, 255], + "secondary_shadow": [228, 228, 228, 255], "main_window_header_background": [10, 8, 80, 255], "main_window_header_button_text_active": [10, 8, 80, 255], From 1a8df9e10ecff985d8ae70ff960b4337e294f721 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 23 Nov 2018 11:46:38 +0100 Subject: [PATCH 068/146] Removed blue border if slicing is not possible CURA-5959 --- resources/qml/ActionPanel/SliceProcessWidget.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/ActionPanel/SliceProcessWidget.qml b/resources/qml/ActionPanel/SliceProcessWidget.qml index 7cce323905..9a9f40ffac 100644 --- a/resources/qml/ActionPanel/SliceProcessWidget.qml +++ b/resources/qml/ActionPanel/SliceProcessWidget.qml @@ -102,7 +102,7 @@ Column color: isSlicing ? UM.Theme.getColor("secondary"): UM.Theme.getColor("primary") textColor: isSlicing ? UM.Theme.getColor("primary"): UM.Theme.getColor("button_text") - + outlineColor: "transparent" disabledColor: UM.Theme.getColor("action_button_disabled") textDisabledColor: UM.Theme.getColor("action_button_disabled_text") shadowEnabled: true From a3bcdaf3b625087204a04867e8a9a374c9bfa857 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Fri, 23 Nov 2018 16:58:57 +0100 Subject: [PATCH 069/146] Make the popup in the printer selector resizable depending on the contents. Also there is a maximum height that will fit 9 printers. Contributes to CURA-5942. --- plugins/PrepareStage/PrepareMenu.qml | 3 + .../qml/PrinterSelector/MachineSelector.qml | 78 ++----------------- .../PrinterSelector/MachineSelectorList.qml | 78 +++++++++++++++++++ 3 files changed, 89 insertions(+), 70 deletions(-) create mode 100644 resources/qml/PrinterSelector/MachineSelectorList.qml diff --git a/plugins/PrepareStage/PrepareMenu.qml b/plugins/PrepareStage/PrepareMenu.qml index eed1da0ad8..a99426acd8 100644 --- a/plugins/PrepareStage/PrepareMenu.qml +++ b/plugins/PrepareStage/PrepareMenu.qml @@ -1,3 +1,6 @@ +// Copyright (c) 2018 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + import QtQuick 2.7 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.4 diff --git a/resources/qml/PrinterSelector/MachineSelector.qml b/resources/qml/PrinterSelector/MachineSelector.qml index a33c54ed88..c73836ad6a 100644 --- a/resources/qml/PrinterSelector/MachineSelector.qml +++ b/resources/qml/PrinterSelector/MachineSelector.qml @@ -82,87 +82,24 @@ Cura.ExpandableComponent { id: popup width: UM.Theme.getSize("machine_selector_widget_content").width - height: UM.Theme.getSize("machine_selector_widget_content").height ScrollView { id: scroll width: parent.width - anchors.top: parent.top - anchors.bottom: separator.top clip: true - Column + MachineSelectorList { - id: column - // Can't use parent.width since the parent is the flickable component and not the ScrollView width: scroll.width - 2 * UM.Theme.getSize("default_lining").width x: UM.Theme.getSize("default_lining").width + property real maximumHeight: UM.Theme.getSize("machine_selector_widget_content").height - buttonRow.height - Label + onHeightChanged: { - text: catalog.i18nc("@label", "Network connected printers") - visible: networkedPrintersModel.items.length > 0 - leftPadding: UM.Theme.getSize("default_margin").width - height: visible ? contentHeight + 2 * UM.Theme.getSize("default_margin").height : 0 - renderType: Text.NativeRendering - font: UM.Theme.getFont("medium") - color: UM.Theme.getColor("text_medium") - verticalAlignment: Text.AlignVCenter - } - - Repeater - { - id: networkedPrinters - - model: UM.ContainerStacksModel - { - id: networkedPrintersModel - filter: {"type": "machine", "um_network_key": "*", "hidden": "False"} - } - - delegate: MachineSelectorButton - { - text: model.metadata["connect_group_name"] - checked: Cura.MachineManager.activeMachineNetworkGroupName == model.metadata["connect_group_name"] - outputDevice: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null - - Connections - { - target: Cura.MachineManager - onActiveMachineNetworkGroupNameChanged: checked = Cura.MachineManager.activeMachineNetworkGroupName == model.metadata["connect_group_name"] - } - } - } - - Label - { - text: catalog.i18nc("@label", "Preset printers") - visible: virtualPrintersModel.items.length > 0 - leftPadding: UM.Theme.getSize("default_margin").width - height: visible ? contentHeight + 2 * UM.Theme.getSize("default_margin").height : 0 - renderType: Text.NativeRendering - font: UM.Theme.getFont("medium") - color: UM.Theme.getColor("text_medium") - verticalAlignment: Text.AlignVCenter - } - - Repeater - { - id: virtualPrinters - - model: UM.ContainerStacksModel - { - id: virtualPrintersModel - filter: {"type": "machine", "um_network_key": null} - } - - delegate: MachineSelectorButton - { - text: model.name - checked: Cura.MachineManager.activeMachineId == model.id - } + scroll.height = Math.min(height, maximumHeight) + popup.height = scroll.height + buttonRow.height } } } @@ -171,7 +108,7 @@ Cura.ExpandableComponent { id: separator - anchors.bottom: buttonRow.top + anchors.top: scroll.bottom width: parent.width height: UM.Theme.getSize("default_lining").height color: UM.Theme.getColor("lining") @@ -181,7 +118,8 @@ Cura.ExpandableComponent { id: buttonRow - anchors.bottom: parent.bottom + // The separator is inside the buttonRow. This is to avoid some weird behaviours with the scroll bar. + anchors.top: separator.top anchors.horizontalCenter: parent.horizontalCenter padding: UM.Theme.getSize("default_margin").width spacing: UM.Theme.getSize("default_margin").width diff --git a/resources/qml/PrinterSelector/MachineSelectorList.qml b/resources/qml/PrinterSelector/MachineSelectorList.qml new file mode 100644 index 0000000000..11a61194b7 --- /dev/null +++ b/resources/qml/PrinterSelector/MachineSelectorList.qml @@ -0,0 +1,78 @@ +// Copyright (c) 2018 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.7 +import QtQuick.Controls 2.3 + +import UM 1.2 as UM +import Cura 1.0 as Cura + +Column +{ + id: machineSelectorList + + Label + { + text: catalog.i18nc("@label", "Network connected printers") + visible: networkedPrintersModel.items.length > 0 + leftPadding: UM.Theme.getSize("default_margin").width + height: visible ? contentHeight + 2 * UM.Theme.getSize("default_margin").height : 0 + renderType: Text.NativeRendering + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text_medium") + verticalAlignment: Text.AlignVCenter + } + + Repeater + { + id: networkedPrinters + + model: UM.ContainerStacksModel + { + id: networkedPrintersModel + filter: {"type": "machine", "um_network_key": "*", "hidden": "False"} + } + + delegate: MachineSelectorButton + { + text: model.metadata["connect_group_name"] + checked: Cura.MachineManager.activeMachineNetworkGroupName == model.metadata["connect_group_name"] + outputDevice: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null + + Connections + { + target: Cura.MachineManager + onActiveMachineNetworkGroupNameChanged: checked = Cura.MachineManager.activeMachineNetworkGroupName == model.metadata["connect_group_name"] + } + } + } + + Label + { + text: catalog.i18nc("@label", "Preset printers") + visible: virtualPrintersModel.items.length > 0 + leftPadding: UM.Theme.getSize("default_margin").width + height: visible ? contentHeight + 2 * UM.Theme.getSize("default_margin").height : 0 + renderType: Text.NativeRendering + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text_medium") + verticalAlignment: Text.AlignVCenter + } + + Repeater + { + id: virtualPrinters + + model: UM.ContainerStacksModel + { + id: virtualPrintersModel + filter: {"type": "machine", "um_network_key": null} + } + + delegate: MachineSelectorButton + { + text: model.name + checked: Cura.MachineManager.activeMachineId == model.id + } + } +} \ No newline at end of file From e1f3e07f049376240293cc2956c0afce4fd18432 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 23 Nov 2018 17:11:20 +0100 Subject: [PATCH 070/146] Changed Marketplace button to no longer use the action button The actionButton is weirdly named and overly used. That being said, the marketplace button is unique, so there is little sense in re-using it --- resources/qml/MainWindow/MainWindowHeader.qml | 35 ++++++++++++------- resources/themes/cura-light/theme.json | 7 ---- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/resources/qml/MainWindow/MainWindowHeader.qml b/resources/qml/MainWindow/MainWindowHeader.qml index 59ec542e6b..ceb27dd726 100644 --- a/resources/qml/MainWindow/MainWindowHeader.qml +++ b/resources/qml/MainWindow/MainWindowHeader.qml @@ -2,6 +2,7 @@ // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.7 +import QtQuick.Controls 2.0 as Controls2 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.1 @@ -73,25 +74,35 @@ Rectangle } // Shortcut button to quick access the Toolbox - Cura.ActionButton + Controls2.Button { + id: marketplaceButton + text: catalog.i18nc("@action:button", "Marketplace") + height: Math.round(0.5 * UM.Theme.getSize("main_window_header").height) + onClicked: Cura.Actions.browsePackages.trigger() + + background: Rectangle + { + radius: UM.Theme.getSize("action_button_radius").width + color: "transparent" + border.width: UM.Theme.getSize("default_lining").width + border.color: UM.Theme.getColor("primary_text") + } + + contentItem: Label + { + id: label + text: marketplaceButton.text + color: UM.Theme.getColor("primary_text") + width: contentWidth + } + anchors { right: accountWidget.left rightMargin: UM.Theme.getSize("default_margin").width verticalCenter: parent.verticalCenter } - leftPadding: UM.Theme.getSize("default_margin").width - rightPadding: UM.Theme.getSize("default_margin").width - text: catalog.i18nc("@action:button", "Marketplace") - height: Math.round(0.5 * UM.Theme.getSize("main_window_header").height) - color: UM.Theme.getColor("main_window_header_secondary_button_background_active") - hoverColor: UM.Theme.getColor("main_window_header_secondary_button_background_hovered") - outlineColor: UM.Theme.getColor("main_window_header_secondary_button_outline_active") - outlineHoverColor: UM.Theme.getColor("main_window_header_secondary_button_outline_hovered") - textColor: UM.Theme.getColor("main_window_header_secondary_button_text_active") - textHoverColor: UM.Theme.getColor("main_window_header_secondary_button_text_hovered") - onClicked: Cura.Actions.browsePackages.trigger() } AccountWidget diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index c5802b6a7e..888bc9bfa6 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -98,13 +98,6 @@ "main_window_header_button_background_inactive": [255, 255, 255, 0], "main_window_header_button_background_hovered": [255, 255, 255, 102], - "main_window_header_secondary_button_text_active": [255, 255, 255, 255], - "main_window_header_secondary_button_text_hovered": [10, 8, 80, 255], - "main_window_header_secondary_button_background_active": [255, 255, 255, 0], - "main_window_header_secondary_button_background_hovered": [255, 255, 255, 255], - "main_window_header_secondary_button_outline_active": [255, 255, 255, 255], - "main_window_header_secondary_button_outline_hovered": [255, 255, 255, 255], - "account_widget_outline_active": [70, 66, 126, 255], "machine_selector_bar": [31, 36, 39, 255], From af1ee535788b4d64945ef578bde3764551297ab4 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 23 Nov 2018 17:41:58 +0100 Subject: [PATCH 071/146] Fix the hover effect of action button CURA-5959 --- resources/qml/ActionButton.qml | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/resources/qml/ActionButton.qml b/resources/qml/ActionButton.qml index 8cd53b5d7e..6dd5839bb9 100644 --- a/resources/qml/ActionButton.qml +++ b/resources/qml/ActionButton.qml @@ -12,7 +12,6 @@ import UM 1.1 as UM Button { id: button - property alias cursorShape: mouseArea.cursorShape property alias iconSource: buttonIcon.source property alias textFont: buttonText.font property alias cornerRadius: backgroundRect.radius @@ -22,12 +21,14 @@ Button property color hoverColor: UM.Theme.getColor("primary_hover") property color disabledColor: color property color textColor: UM.Theme.getColor("button_text") - property color textHoverColor: UM.Theme.getColor("button_text_hover") + property color textHoverColor: textColor property color textDisabledColor: textColor property color outlineColor: color property color outlineHoverColor: hoverColor property color outlineDisabledColor: outlineColor + hoverEnabled: true + property alias shadowColor: shadow.color property alias shadowEnabled: shadow.visible @@ -95,13 +96,4 @@ Button delay: 500 visible: text != "" && button.hovered } - - MouseArea - { - id: mouseArea - anchors.fill: parent - // Ensure that the button will still accept the clicks on it's own. - onPressed: mouse.accepted = false - hoverEnabled: true - } } From b82ea58bc815b28815cd6991230d5a3b4d8ca78d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 23 Nov 2018 18:07:50 +0100 Subject: [PATCH 072/146] Changed all the action buttons to either use primary or secondary button CURA-5959 --- resources/qml/Account/GeneralOperations.qml | 8 +-- resources/qml/Account/UserOperations.qml | 8 +-- .../ActionPanel/OutputDevicesActionButton.qml | 5 +- .../qml/ActionPanel/OutputProcessWidget.qml | 9 +--- .../qml/ActionPanel/SliceProcessWidget.qml | 49 ++++++++++--------- resources/qml/PrimaryButton.qml | 18 +++++++ resources/qml/SecondaryButton.qml | 18 +++++++ resources/themes/cura-light/theme.json | 10 ++++ 8 files changed, 81 insertions(+), 44 deletions(-) create mode 100644 resources/qml/PrimaryButton.qml create mode 100644 resources/qml/SecondaryButton.qml diff --git a/resources/qml/Account/GeneralOperations.qml b/resources/qml/Account/GeneralOperations.qml index 4614c4ba88..b9f1025d5e 100644 --- a/resources/qml/Account/GeneralOperations.qml +++ b/resources/qml/Account/GeneralOperations.qml @@ -11,20 +11,16 @@ Row { spacing: UM.Theme.getSize("default_margin").width - Cura.ActionButton + Cura.SecondaryButton { width: UM.Theme.getSize("account_button").width height: UM.Theme.getSize("account_button").height text: catalog.i18nc("@button", "Create account") - color: UM.Theme.getColor("secondary") - hoverColor: UM.Theme.getColor("secondary") - textColor: UM.Theme.getColor("main_window_header_button_text_active") - textHoverColor: UM.Theme.getColor("main_window_header_button_text_active") onClicked: Qt.openUrlExternally("https://account.ultimaker.com/app/create") fixedWidthMode: true } - Cura.ActionButton + Cura.PrimaryButton { width: UM.Theme.getSize("account_button").width height: UM.Theme.getSize("account_button").height diff --git a/resources/qml/Account/UserOperations.qml b/resources/qml/Account/UserOperations.qml index c167813425..b9ffa395d6 100644 --- a/resources/qml/Account/UserOperations.qml +++ b/resources/qml/Account/UserOperations.qml @@ -11,20 +11,16 @@ Row { spacing: UM.Theme.getSize("default_margin").width - Cura.ActionButton + Cura.SecondaryButton { width: UM.Theme.getSize("account_button").width height: UM.Theme.getSize("account_button").height text: catalog.i18nc("@button", "Manage account") - color: UM.Theme.getColor("secondary") - hoverColor: UM.Theme.getColor("secondary") - textColor: UM.Theme.getColor("main_window_header_button_text_active") - textHoverColor: UM.Theme.getColor("main_window_header_button_text_active") onClicked: Qt.openUrlExternally("https://account.ultimaker.com") fixedWidthMode: true } - Cura.ActionButton + Cura.PrimaryButton { width: UM.Theme.getSize("account_button").width height: UM.Theme.getSize("account_button").height diff --git a/resources/qml/ActionPanel/OutputDevicesActionButton.qml b/resources/qml/ActionPanel/OutputDevicesActionButton.qml index 9682dddf14..d24d440241 100644 --- a/resources/qml/ActionPanel/OutputDevicesActionButton.qml +++ b/resources/qml/ActionPanel/OutputDevicesActionButton.qml @@ -12,13 +12,12 @@ Item { id: widget - Cura.ActionButton + Cura.PrimaryButton { id: saveToButton height: parent.height fixedWidthMode: true - shadowEnabled: true - shadowColor: UM.Theme.getColor("primary_shadow") + anchors { top: parent.top diff --git a/resources/qml/ActionPanel/OutputProcessWidget.qml b/resources/qml/ActionPanel/OutputProcessWidget.qml index 79b9898e49..ddbe709a84 100644 --- a/resources/qml/ActionPanel/OutputProcessWidget.qml +++ b/resources/qml/ActionPanel/OutputProcessWidget.qml @@ -101,18 +101,13 @@ Column spacing: UM.Theme.getSize("default_margin").width width: parent.width - Cura.ActionButton + Cura.SecondaryButton { id: previewStageShortcut - leftPadding: UM.Theme.getSize("default_margin").width - rightPadding: UM.Theme.getSize("default_margin").width height: UM.Theme.getSize("action_panel_button").height text: catalog.i18nc("@button", "Preview") - color: UM.Theme.getColor("secondary") - hoverColor: UM.Theme.getColor("secondary") - textColor: UM.Theme.getColor("primary") - textHoverColor: UM.Theme.getColor("text") + onClicked: UM.Controller.setActiveStage("PreviewStage") visible: UM.Controller.activeStage != null && UM.Controller.activeStage.stageId != "PreviewStage" diff --git a/resources/qml/ActionPanel/SliceProcessWidget.qml b/resources/qml/ActionPanel/SliceProcessWidget.qml index 9a9f40ffac..199b94ab33 100644 --- a/resources/qml/ActionPanel/SliceProcessWidget.qml +++ b/resources/qml/ActionPanel/SliceProcessWidget.qml @@ -81,36 +81,41 @@ Column } } - Cura.ActionButton - { - id: prepareButton - width: parent.width - height: UM.Theme.getSize("action_panel_button").height - fixedWidthMode: true + Item + { + id: prepareButtons // Get the current value from the preferences property bool autoSlice: UM.Preferences.getValue("general/auto_slice") // Disable the slice process when - property bool disabledSlice: [UM.Backend.Done, UM.Backend.Error].indexOf(widget.backendState) != -1 - property bool isSlicing: [UM.Backend.NotStarted, UM.Backend.Error].indexOf(widget.backendState) == -1 - - text: isSlicing ? catalog.i18nc("@button", "Cancel") : catalog.i18nc("@button", "Slice") - - enabled: !autoSlice && !disabledSlice + width: parent.width + height: UM.Theme.getSize("action_panel_button").height visible: !autoSlice + Cura.PrimaryButton + { + id: sliceButton + fixedWidthMode: true + anchors.fill: parent + text: catalog.i18nc("@button", "Slice") + enabled: !autoSlice && widget.backendState != UM.Backend.Error + visible: widget.backendState == UM.Backend.NotStarted || widget.backendState == UM.Backend.Error + onClicked: sliceOrStopSlicing() + } - color: isSlicing ? UM.Theme.getColor("secondary"): UM.Theme.getColor("primary") - textColor: isSlicing ? UM.Theme.getColor("primary"): UM.Theme.getColor("button_text") - outlineColor: "transparent" - disabledColor: UM.Theme.getColor("action_button_disabled") - textDisabledColor: UM.Theme.getColor("action_button_disabled_text") - shadowEnabled: true - shadowColor: isSlicing ? UM.Theme.getColor("secondary_shadow") : enabled ? UM.Theme.getColor("action_button_shadow"): UM.Theme.getColor("action_button_disabled_shadow") - - onClicked: sliceOrStopSlicing() + Cura.SecondaryButton + { + id: cancelButton + fixedWidthMode: true + anchors.fill: parent + text: catalog.i18nc("@button", "Cancel") + enabled: sliceButton.enabled + visible: !sliceButton.visible + onClicked: sliceOrStopSlicing() + } } + // React when the user changes the preference of having the auto slice enabled Connections { @@ -118,7 +123,7 @@ Column onPreferenceChanged: { var autoSlice = UM.Preferences.getValue("general/auto_slice") - prepareButton.autoSlice = autoSlice + prepareButtons.autoSlice = autoSlice } } diff --git a/resources/qml/PrimaryButton.qml b/resources/qml/PrimaryButton.qml new file mode 100644 index 0000000000..8450e524e2 --- /dev/null +++ b/resources/qml/PrimaryButton.qml @@ -0,0 +1,18 @@ +import QtQuick 2.2 +import QtQuick.Controls 1.1 + +import UM 1.4 as UM +import Cura 1.1 as Cura + + +Cura.ActionButton +{ + shadowEnabled: true + shadowColor: enabled ? UM.Theme.getColor("primary_button_shadow"): UM.Theme.getColor("action_button_disabled_shadow") + color: UM.Theme.getColor("primary_button") + textColor: UM.Theme.getColor("primary_button_text") + outlineColor: "transparent" + disabledColor: UM.Theme.getColor("action_button_disabled") + textDisabledColor: UM.Theme.getColor("action_button_disabled_text") + hoverColor: UM.Theme.getColor("primary_button_hover") +} \ No newline at end of file diff --git a/resources/qml/SecondaryButton.qml b/resources/qml/SecondaryButton.qml new file mode 100644 index 0000000000..0e6b79b3a7 --- /dev/null +++ b/resources/qml/SecondaryButton.qml @@ -0,0 +1,18 @@ +import QtQuick 2.2 +import QtQuick.Controls 1.1 + +import UM 1.4 as UM +import Cura 1.1 as Cura + + +Cura.ActionButton +{ + shadowEnabled: true + shadowColor: enabled ? UM.Theme.getColor("secondary_button_shadow"): UM.Theme.getColor("action_button_disabled_shadow") + color: UM.Theme.getColor("secondary_button") + textColor: UM.Theme.getColor("secondary_button_text") + outlineColor: "transparent" + disabledColor: UM.Theme.getColor("action_button_disabled") + textDisabledColor: UM.Theme.getColor("action_button_disabled_text") + hoverColor: UM.Theme.getColor("secondary_button_hover") +} \ No newline at end of file diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 888bc9bfa6..cbdc37caa1 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -90,6 +90,16 @@ "secondary": [245, 245, 245, 255], "secondary_shadow": [228, 228, 228, 255], + "primary_button": [38,113,231,255], + "primary_button_shadow": [27,95,202, 255], + "primary_button_hover": [81,145,247, 255], + "primary_button_text": [255, 255, 255, 255], + + "secondary_button": [240,240,240, 255], + "secondary_button_shadow": [228, 228, 228, 255], + "secondary_button_hover": [228,228,228, 255], + "secondary_button_text": [30,102,215, 255], + "main_window_header_background": [10, 8, 80, 255], "main_window_header_button_text_active": [10, 8, 80, 255], "main_window_header_button_text_inactive": [255, 255, 255, 255], From 9e92542eeec397440f2f383d68650c291e30c8d7 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Sun, 25 Nov 2018 18:14:01 +0100 Subject: [PATCH 073/146] Fix the height of the IconLabel to avoid getting binding loops. Contributes to CURA-5942. --- resources/qml/ActionPanel/OutputProcessWidget.qml | 2 -- resources/qml/ActionPanel/SliceProcessWidget.qml | 1 - resources/qml/IconLabel.qml | 2 ++ 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/resources/qml/ActionPanel/OutputProcessWidget.qml b/resources/qml/ActionPanel/OutputProcessWidget.qml index 87f9d2015d..3c4386f079 100644 --- a/resources/qml/ActionPanel/OutputProcessWidget.qml +++ b/resources/qml/ActionPanel/OutputProcessWidget.qml @@ -47,7 +47,6 @@ Column { id: estimatedTime width: parent.width - height: childrenRect.height text: PrintInformation.currentPrintTime.getDisplayString(UM.DurationFormat.Long) source: UM.Theme.getIcon("clock") @@ -58,7 +57,6 @@ Column { id: estimatedCosts width: parent.width - height: childrenRect.height property var printMaterialLengths: PrintInformation.materialLengths property var printMaterialWeights: PrintInformation.materialWeights diff --git a/resources/qml/ActionPanel/SliceProcessWidget.qml b/resources/qml/ActionPanel/SliceProcessWidget.qml index 9c46539220..2d4a7b6b89 100644 --- a/resources/qml/ActionPanel/SliceProcessWidget.qml +++ b/resources/qml/ActionPanel/SliceProcessWidget.qml @@ -44,7 +44,6 @@ Column { id: message width: parent.width - height: childrenRect.height visible: widget.backendState == UM.Backend.Error text: catalog.i18nc("@label:PrintjobStatus", "Unable to Slice") diff --git a/resources/qml/IconLabel.qml b/resources/qml/IconLabel.qml index 90930e91c7..0941254e7b 100644 --- a/resources/qml/IconLabel.qml +++ b/resources/qml/IconLabel.qml @@ -18,6 +18,8 @@ Item property alias font: label.font property alias iconSize: icon.width + implicitHeight: icon.height + UM.RecolorImage { id: icon From f3bf20ca1b8f17a9b63aa14f57df56a2e17b95f4 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Sun, 25 Nov 2018 18:24:21 +0100 Subject: [PATCH 074/146] Separate the view selector into a different file. --- plugins/PreviewStage/PreviewMenu.qml | 72 +--------------- .../qml/PrinterSelector/MachineSelector.qml | 2 - resources/qml/ViewsSelector.qml | 82 +++++++++++++++++++ resources/qml/qmldir | 3 +- 4 files changed, 86 insertions(+), 73 deletions(-) create mode 100644 resources/qml/ViewsSelector.qml diff --git a/plugins/PreviewStage/PreviewMenu.qml b/plugins/PreviewStage/PreviewMenu.qml index d660db549b..a1f59cd4ca 100644 --- a/plugins/PreviewStage/PreviewMenu.qml +++ b/plugins/PreviewStage/PreviewMenu.qml @@ -29,80 +29,12 @@ Item anchors.centerIn: parent height: parent.height - Cura.ExpandableComponent + Cura.ViewsSelector { - id: viewSelector - iconSource: expanded ? UM.Theme.getIcon("arrow_bottom") : UM.Theme.getIcon("arrow_left") + id: viewsSelector height: parent.height width: UM.Theme.getSize("views_selector").width headerCornerSide: Cura.RoundedRectangle.Direction.Left - - property var viewModel: UM.ViewModel { } - - property var activeView: - { - for (var i = 0; i < viewModel.rowCount(); i++) - { - if (viewModel.items[i].active) - { - return viewModel.items[i] - } - } - return null - } - - Component.onCompleted: - { - // Nothing was active, so just return the first one (the list is sorted by priority, so the most - // important one should be returned) - if (activeView == null) - { - UM.Controller.setActiveView(viewModel.getItem(0).id) - } - } - - headerItem: Label - { - text: viewSelector.activeView ? viewSelector.activeView.name : "" - verticalAlignment: Text.AlignVCenter - height: parent.height - elide: Text.ElideRight - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - renderType: Text.NativeRendering - } - - popupItem: Column - { - id: viewSelectorPopup - width: viewSelector.width - 2 * UM.Theme.getSize("default_margin").width - - // For some reason the height/width of the column gets set to 0 if this is not set... - Component.onCompleted: - { - height = implicitHeight - width = viewSelector.width - 2 * UM.Theme.getSize("default_margin").width - } - - Repeater - { - id: viewsList - model: viewSelector.viewModel - RoundButton - { - text: name - radius: UM.Theme.getSize("default_radius").width - checkable: true - checked: viewSelector.activeView != null ? viewSelector.activeView.id == id : false - onClicked: - { - viewSelector.togglePopup() - UM.Controller.setActiveView(id) - } - } - } - - } } // Separator line diff --git a/resources/qml/PrinterSelector/MachineSelector.qml b/resources/qml/PrinterSelector/MachineSelector.qml index c73836ad6a..b14734dfb1 100644 --- a/resources/qml/PrinterSelector/MachineSelector.qml +++ b/resources/qml/PrinterSelector/MachineSelector.qml @@ -3,8 +3,6 @@ import QtQuick 2.7 import QtQuick.Controls 2.3 -import QtQuick.Controls.Styles 1.1 -import QtQuick.Layouts 1.1 import UM 1.2 as UM import Cura 1.0 as Cura diff --git a/resources/qml/ViewsSelector.qml b/resources/qml/ViewsSelector.qml new file mode 100644 index 0000000000..0e11cccc5a --- /dev/null +++ b/resources/qml/ViewsSelector.qml @@ -0,0 +1,82 @@ +// Copyright (c) 2018 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.7 +import QtQuick.Controls 2.3 + +import UM 1.2 as UM +import Cura 1.0 as Cura + +Cura.ExpandableComponent +{ + id: viewSelector + + iconSource: expanded ? UM.Theme.getIcon("arrow_bottom") : UM.Theme.getIcon("arrow_left") + + property var viewModel: UM.ViewModel { } + + property var activeView: + { + for (var i = 0; i < viewModel.rowCount(); i++) + { + if (viewModel.items[i].active) + { + return viewModel.items[i] + } + } + return null + } + + Component.onCompleted: + { + // Nothing was active, so just return the first one (the list is sorted by priority, so the most + // important one should be returned) + if (activeView == null) + { + UM.Controller.setActiveView(viewModel.getItem(0).id) + } + } + + headerItem: Label + { + text: viewSelector.activeView ? viewSelector.activeView.name : "" + verticalAlignment: Text.AlignVCenter + height: parent.height + elide: Text.ElideRight + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + renderType: Text.NativeRendering + } + + popupItem: Column + { + id: viewSelectorPopup + width: viewSelector.width - 2 * UM.Theme.getSize("default_margin").width + + // For some reason the height/width of the column gets set to 0 if this is not set... + Component.onCompleted: + { + height = implicitHeight + width = viewSelector.width - 2 * UM.Theme.getSize("default_margin").width + } + + Repeater + { + id: viewsList + model: viewSelector.viewModel + RoundButton + { + text: name + radius: UM.Theme.getSize("default_radius").width + checkable: true + checked: viewSelector.activeView != null ? viewSelector.activeView.id == id : false + onClicked: + { + viewSelector.togglePopup() + UM.Controller.setActiveView(id) + } + } + } + + } +} \ No newline at end of file diff --git a/resources/qml/qmldir b/resources/qml/qmldir index 67388100ca..43ae4411af 100644 --- a/resources/qml/qmldir +++ b/resources/qml/qmldir @@ -11,4 +11,5 @@ ActionPanelWidget 1.0 ActionPanelWidget.qml IconLabel 1.0 IconLabel.qml OutputDevicesActionButton 1.0 OutputDevicesActionButton.qml ExpandableComponent 1.0 ExpandableComponent.qml -PrinterTypeLabel 1.0 PrinterTypeLabel.qml \ No newline at end of file +PrinterTypeLabel 1.0 PrinterTypeLabel.qml +ViewsSelector 1.0 ViewsSelector.qml \ No newline at end of file From 68c96a25775c6be13bdcdba64544e68837cc41c2 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Sun, 25 Nov 2018 19:42:00 +0100 Subject: [PATCH 075/146] Modify the header item of the view selector. --- resources/qml/ViewsSelector.qml | 36 +++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/resources/qml/ViewsSelector.qml b/resources/qml/ViewsSelector.qml index 0e11cccc5a..b9048803a2 100644 --- a/resources/qml/ViewsSelector.qml +++ b/resources/qml/ViewsSelector.qml @@ -37,15 +37,35 @@ Cura.ExpandableComponent } } - headerItem: Label + headerItem: Item { - text: viewSelector.activeView ? viewSelector.activeView.name : "" - verticalAlignment: Text.AlignVCenter - height: parent.height - elide: Text.ElideRight - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - renderType: Text.NativeRendering + Label + { + id: title + text: catalog.i18nc("@button", "View types") + verticalAlignment: Text.AlignVCenter + height: parent.height + elide: Text.ElideRight + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text_medium") + renderType: Text.NativeRendering + } + + Label + { + text: viewSelector.activeView ? viewSelector.activeView.name : "" + verticalAlignment: Text.AlignVCenter + anchors + { + left: title.right + leftMargin: UM.Theme.getSize("default_margin").width + } + height: parent.height + elide: Text.ElideRight + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + renderType: Text.NativeRendering + } } popupItem: Column From c5d0ed26513e12045b2fb84d6408a79f729a81df Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Sun, 25 Nov 2018 20:13:56 +0100 Subject: [PATCH 076/146] Define the look and feel of the view selector button. Adjust the sizes. --- .../qml/PrinterSelector/MachineSelector.qml | 5 +-- resources/qml/ViewsSelector.qml | 39 ++++++++++++++++--- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/resources/qml/PrinterSelector/MachineSelector.qml b/resources/qml/PrinterSelector/MachineSelector.qml index b14734dfb1..b6657f112e 100644 --- a/resources/qml/PrinterSelector/MachineSelector.qml +++ b/resources/qml/PrinterSelector/MachineSelector.qml @@ -15,7 +15,7 @@ Cura.ExpandableComponent property bool isPrinterConnected: Cura.MachineManager.printerConnected property var outputDevice: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null - popupPadding: 0 + popupPadding: UM.Theme.getSize("default_lining").width popupAlignment: Cura.ExpandableComponent.PopupAlignment.AlignLeft iconSource: expanded ? UM.Theme.getIcon("arrow_bottom") : UM.Theme.getIcon("arrow_left") @@ -90,8 +90,7 @@ Cura.ExpandableComponent MachineSelectorList { // Can't use parent.width since the parent is the flickable component and not the ScrollView - width: scroll.width - 2 * UM.Theme.getSize("default_lining").width - x: UM.Theme.getSize("default_lining").width + width: scroll.width property real maximumHeight: UM.Theme.getSize("machine_selector_widget_content").height - buttonRow.height onHeightChanged: diff --git a/resources/qml/ViewsSelector.qml b/resources/qml/ViewsSelector.qml index b9048803a2..e9fdd57177 100644 --- a/resources/qml/ViewsSelector.qml +++ b/resources/qml/ViewsSelector.qml @@ -11,6 +11,8 @@ Cura.ExpandableComponent { id: viewSelector + popupPadding: UM.Theme.getSize("default_lining").width + popupAlignment: Cura.ExpandableComponent.PopupAlignment.AlignLeft iconSource: expanded ? UM.Theme.getIcon("arrow_bottom") : UM.Theme.getIcon("arrow_left") property var viewModel: UM.ViewModel { } @@ -71,25 +73,51 @@ Cura.ExpandableComponent popupItem: Column { id: viewSelectorPopup - width: viewSelector.width - 2 * UM.Theme.getSize("default_margin").width + width: viewSelector.width - 2 * viewSelector.popupPadding // For some reason the height/width of the column gets set to 0 if this is not set... Component.onCompleted: { height = implicitHeight - width = viewSelector.width - 2 * UM.Theme.getSize("default_margin").width + width = viewSelector.width - 2 * viewSelector.popupPadding } Repeater { id: viewsList model: viewSelector.viewModel - RoundButton + + delegate: Button { - text: name - radius: UM.Theme.getSize("default_radius").width + id: viewsSelectorButton + text: model.name + width: parent.width + height: UM.Theme.getSize("action_button").height + leftPadding: UM.Theme.getSize("default_margin").width + rightPadding: UM.Theme.getSize("default_margin").width checkable: true checked: viewSelector.activeView != null ? viewSelector.activeView.id == id : false + + contentItem: Label + { + id: buttonText + text: viewsSelectorButton.text + color: UM.Theme.getColor("text") + font: UM.Theme.getFont("action_button") + renderType: Text.NativeRendering + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + background: Rectangle + { + id: backgroundRect + color: viewsSelectorButton.hovered ? UM.Theme.getColor("action_button_hovered") : "transparent" + radius: UM.Theme.getSize("action_button_radius").width + border.width: UM.Theme.getSize("default_lining").width + border.color: viewsSelectorButton.checked ? UM.Theme.getColor("primary") : "transparent" + } + onClicked: { viewSelector.togglePopup() @@ -97,6 +125,5 @@ Cura.ExpandableComponent } } } - } } \ No newline at end of file From 8f9d6bee6243f32c9d966202e1f9a4ea5086b611 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 26 Nov 2018 08:30:24 +0100 Subject: [PATCH 077/146] Document dependency on fdm_materials Without it, Cura simply won't start if you had a printer loaded with material profiles. Fixes #4460. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 70466e9c22..93abcc0c61 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,9 @@ Dependencies ------------ * [Uranium](https://github.com/Ultimaker/Uranium) Cura is built on top of the Uranium framework. * [CuraEngine](https://github.com/Ultimaker/CuraEngine) This will be needed at runtime to perform the actual slicing. +* [fdm_materials](https://github.com/Ultimaker/fdm_materials) Required to load a printer that has swappable material profiles. * [PySerial](https://github.com/pyserial/pyserial) Only required for USB printing support. -* [python-zeroconf](https://github.com/jstasiak/python-zeroconf) Only required to detect mDNS-enabled printers +* [python-zeroconf](https://github.com/jstasiak/python-zeroconf) Only required to detect mDNS-enabled printers. Build scripts ------------- From c9eb57ceade38f567fc49f5e5ad0f6f1c8913d79 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 26 Nov 2018 09:32:59 +0100 Subject: [PATCH 078/146] Don't copy preference file to the same location This crashes Cura on start-up for some people. --- cura/Backups/Backup.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 897d5fa979..80681acb4f 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -46,12 +46,13 @@ class Backup: # We copy the preferences file to the user data directory in Linux as it's in a different location there. # When restoring a backup on Linux, we move it back. - if Platform.isLinux(): + if Platform.isLinux(): #TODO: This should check for the config directory not being the same as the data directory, rather than hard-coding that to Linux systems. preferences_file_name = self._application.getApplicationName() preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name)) backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name)) - Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file) - shutil.copyfile(preferences_file, backup_preferences_file) + if not os.path.samefile(preferences_file, backup_preferences_file): + Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file) + shutil.copyfile(preferences_file, backup_preferences_file) # Create an empty buffer and write the archive to it. buffer = io.BytesIO() From 09af7a9435178bc9a6e811c7ade4889880a745f9 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 26 Nov 2018 09:34:27 +0100 Subject: [PATCH 079/146] Slice button will now be disabled on error CURA-5959 --- resources/qml/ActionPanel/SliceProcessWidget.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/qml/ActionPanel/SliceProcessWidget.qml b/resources/qml/ActionPanel/SliceProcessWidget.qml index 199b94ab33..05a9345585 100644 --- a/resources/qml/ActionPanel/SliceProcessWidget.qml +++ b/resources/qml/ActionPanel/SliceProcessWidget.qml @@ -42,7 +42,7 @@ Column Cura.IconLabel { - id: message + id: unableToSliceMessage width: parent.width visible: widget.backendState == UM.Backend.Error @@ -98,7 +98,7 @@ Column fixedWidthMode: true anchors.fill: parent text: catalog.i18nc("@button", "Slice") - enabled: !autoSlice && widget.backendState != UM.Backend.Error + enabled: widget.backendState != UM.Backend.Error visible: widget.backendState == UM.Backend.NotStarted || widget.backendState == UM.Backend.Error onClicked: sliceOrStopSlicing() } From 5469613c17c0caa34b58750a1aeeb664333886ca Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 26 Nov 2018 09:40:14 +0100 Subject: [PATCH 080/146] Don't fail the samefile check if second file didn't exist If the backup file didn't exist but the original did, then apparently they are not the same file so the copy should be allowed. --- cura/Backups/Backup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 80681acb4f..714d6527fe 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -50,7 +50,7 @@ class Backup: preferences_file_name = self._application.getApplicationName() preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name)) backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name)) - if not os.path.samefile(preferences_file, backup_preferences_file): + if os.path.exists(preferences_file) and (not os.path.exists(backup_preferences_file) or not os.path.samefile(preferences_file, backup_preferences_file)): Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file) shutil.copyfile(preferences_file, backup_preferences_file) From c8e065d7a78f3d62b26cded3cd0063e5a05fead4 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 26 Nov 2018 10:21:15 +0100 Subject: [PATCH 081/146] Fix code-style Contributes to CURA-5942. Co-Authored-By: diegopradogesto --- resources/qml/ExpandableComponent.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/qml/ExpandableComponent.qml b/resources/qml/ExpandableComponent.qml index 80c65f12a7..ccfb9c6da2 100644 --- a/resources/qml/ExpandableComponent.qml +++ b/resources/qml/ExpandableComponent.qml @@ -12,7 +12,8 @@ Item id: base // Enumeration with the different possible alignments of the popup with respect of the headerItem - enum PopupAlignment { + enum PopupAlignment + { AlignLeft, AlignRight } From 5b940b524204ca56b5fafe05bad28d5e477b8ea7 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Mon, 26 Nov 2018 10:28:36 +0100 Subject: [PATCH 082/146] Use thick margin and change some anchors. Contributes to CURA-5942. --- resources/qml/PrinterSelector/MachineSelector.qml | 8 ++++++-- resources/qml/PrinterSelector/MachineSelectorButton.qml | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/resources/qml/PrinterSelector/MachineSelector.qml b/resources/qml/PrinterSelector/MachineSelector.qml index b6657f112e..d721ed5b83 100644 --- a/resources/qml/PrinterSelector/MachineSelector.qml +++ b/resources/qml/PrinterSelector/MachineSelector.qml @@ -48,8 +48,12 @@ Cura.ExpandableComponent { id: icon - anchors.bottom: parent.bottom - x: UM.Theme.getSize("thick_margin").width + anchors + { + bottom: parent.bottom + left: parent.left + leftMargin: UM.Theme.getSize("thick_margin").width + } source: UM.Theme.getIcon("printer_connected") width: UM.Theme.getSize("printer_status_icon").width diff --git a/resources/qml/PrinterSelector/MachineSelectorButton.qml b/resources/qml/PrinterSelector/MachineSelectorButton.qml index e7b44a4447..fef6867e35 100644 --- a/resources/qml/PrinterSelector/MachineSelectorButton.qml +++ b/resources/qml/PrinterSelector/MachineSelectorButton.qml @@ -13,8 +13,8 @@ Button width: parent.width height: UM.Theme.getSize("action_button").height - leftPadding: Math.round(1.5 * UM.Theme.getSize("default_margin").width) - rightPadding: Math.round(1.5 * UM.Theme.getSize("default_margin").width) + leftPadding: UM.Theme.getSize("thick_margin").width + rightPadding: UM.Theme.getSize("thick_margin").width checkable: true property var outputDevice: null From 63fab9f038e8615432a8c592138e915351131386 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Mon, 26 Nov 2018 10:49:43 +0100 Subject: [PATCH 083/146] Use theme sizes. Contributes to CURA-5942. --- resources/qml/PrinterSelector/MachineSelector.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/qml/PrinterSelector/MachineSelector.qml b/resources/qml/PrinterSelector/MachineSelector.qml index d721ed5b83..120ce02edd 100644 --- a/resources/qml/PrinterSelector/MachineSelector.qml +++ b/resources/qml/PrinterSelector/MachineSelector.qml @@ -71,8 +71,8 @@ Cura.ExpandableComponent id: iconBackground anchors.centerIn: parent // Make it a bit bigger so there is an outline - width: parent.width + 2 - height: parent.height + 2 + width: parent.width + 2 * UM.Theme.getSize("default_lining").width + height: parent.height + 2 * UM.Theme.getSize("default_lining").height radius: Math.round(width / 2) color: UM.Theme.getColor("main_background") z: parent.z - 1 From fa1ef5c45c70ebdebaef3d9d19b026e6586523b2 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Mon, 26 Nov 2018 10:51:08 +0100 Subject: [PATCH 084/146] Rename function name to be more clear to what it does. Contributes to CURA-5942. --- resources/qml/PrinterSelector/MachineSelectorButton.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/qml/PrinterSelector/MachineSelectorButton.qml b/resources/qml/PrinterSelector/MachineSelectorButton.qml index fef6867e35..992ea55b1d 100644 --- a/resources/qml/PrinterSelector/MachineSelectorButton.qml +++ b/resources/qml/PrinterSelector/MachineSelectorButton.qml @@ -20,7 +20,7 @@ Button property var outputDevice: null property var printerTypesList: [] - function setPrinterTypesList() + function updatePrinterTypesList() { printerTypesList = (checked && (outputDevice != null)) ? outputDevice.uniquePrinterTypes : [] } @@ -97,14 +97,14 @@ Button Connections { target: outputDevice - onUniqueConfigurationsChanged: setPrinterTypesList() + onUniqueConfigurationsChanged: updatePrinterTypesList() } Connections { target: Cura.MachineManager - onOutputDevicesChanged: setPrinterTypesList() + onOutputDevicesChanged: updatePrinterTypesList() } - Component.onCompleted: setPrinterTypesList() + Component.onCompleted: updatePrinterTypesList() } From 4990f205662aeba91bce43c57fe41a94abd1d545 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 26 Nov 2018 10:55:32 +0100 Subject: [PATCH 085/146] Use QStringList instead of QVariantList since the return value is a list of strings. Co-Authored-By: diegopradogesto --- cura/PrinterOutputDevice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index f8a663f0e4..99c48189cc 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -212,7 +212,7 @@ class PrinterOutputDevice(QObject, OutputDevice): self.uniqueConfigurationsChanged.emit() # Returns the unique configurations of the printers within this output device - @pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged) + @pyqtProperty("QStringList", notify = uniqueConfigurationsChanged) def uniquePrinterTypes(self) -> List[str]: return list(set([configuration.printerType for configuration in self._unique_configurations])) @@ -243,4 +243,4 @@ class PrinterOutputDevice(QObject, OutputDevice): if not self._firmware_updater: return - self._firmware_updater.updateFirmware(firmware_file) \ No newline at end of file + self._firmware_updater.updateFirmware(firmware_file) From 21bfa6e4c20819e09b8cb2f27f62427dc71c8476 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Mon, 26 Nov 2018 10:58:38 +0100 Subject: [PATCH 086/146] Fix code style. Add brackets in new line. Contributes to CURA-5942. --- resources/qml/PrinterSelector/MachineSelectorList.qml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/resources/qml/PrinterSelector/MachineSelectorList.qml b/resources/qml/PrinterSelector/MachineSelectorList.qml index 11a61194b7..5ef04b7351 100644 --- a/resources/qml/PrinterSelector/MachineSelectorList.qml +++ b/resources/qml/PrinterSelector/MachineSelectorList.qml @@ -30,7 +30,10 @@ Column model: UM.ContainerStacksModel { id: networkedPrintersModel - filter: {"type": "machine", "um_network_key": "*", "hidden": "False"} + filter: + { + "type": "machine", "um_network_key": "*", "hidden": "False" + } } delegate: MachineSelectorButton @@ -66,7 +69,10 @@ Column model: UM.ContainerStacksModel { id: virtualPrintersModel - filter: {"type": "machine", "um_network_key": null} + filter: + { + "type": "machine", "um_network_key": null + } } delegate: MachineSelectorButton From 72d972c8b428e8ad8d6bf0b831b67d6301058049 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Mon, 26 Nov 2018 11:03:58 +0100 Subject: [PATCH 087/146] Remove PrinterStatusIcon that is not used anymore. Contributes to CURA-5942. --- resources/qml/Menus/PrinterStatusIcon.qml | 27 ----------------------- 1 file changed, 27 deletions(-) delete mode 100644 resources/qml/Menus/PrinterStatusIcon.qml diff --git a/resources/qml/Menus/PrinterStatusIcon.qml b/resources/qml/Menus/PrinterStatusIcon.qml deleted file mode 100644 index 6ff6b07af8..0000000000 --- a/resources/qml/Menus/PrinterStatusIcon.qml +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2017 Ultimaker B.V. -// Cura is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.2 - -import UM 1.2 as UM -import Cura 1.0 as Cura - -Item -{ - property var status: "disconnected" - width: childrenRect.width - height: childrenRect.height - UM.RecolorImage - { - id: statusIcon - width: UM.Theme.getSize("printer_status_icon").width - height: UM.Theme.getSize("printer_status_icon").height - sourceSize.width: width - sourceSize.height: width - color: UM.Theme.getColor("tab_status_" + parent.status) - source: UM.Theme.getIcon(parent.status) - } -} - - - From 5397cda2b4d3077a76ca5a3d9444e09cf386a318 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Mon, 26 Nov 2018 12:00:06 +0100 Subject: [PATCH 088/146] Add a label to the action panel when auto slice is ON, indicating that the process runs automatically. Contributes to CURA-5942. --- resources/qml/ActionPanel/SliceProcessWidget.qml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/resources/qml/ActionPanel/SliceProcessWidget.qml b/resources/qml/ActionPanel/SliceProcessWidget.qml index 05a9345585..3329ac4b23 100644 --- a/resources/qml/ActionPanel/SliceProcessWidget.qml +++ b/resources/qml/ActionPanel/SliceProcessWidget.qml @@ -40,6 +40,18 @@ Column } } + Label + { + id: autoSlicingLabel + width: parent.width + visible: prepareButtons.autoSlice && widget.backendState == UM.Backend.Processing + + text: catalog.i18nc("@label:PrintjobStatus", "Auto slicing...") + color: UM.Theme.getColor("text") + font: UM.Theme.getFont("very_small") + renderType: Text.NativeRendering + } + Cura.IconLabel { id: unableToSliceMessage From eea6490e75cea6cd30555edc674bd58969fcd72b Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 26 Nov 2018 13:27:26 +0100 Subject: [PATCH 089/146] Hover of output device now spans the popup CURA-5959 --- resources/qml/ActionPanel/OutputDevicesActionButton.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/qml/ActionPanel/OutputDevicesActionButton.qml b/resources/qml/ActionPanel/OutputDevicesActionButton.qml index d24d440241..e4b4884794 100644 --- a/resources/qml/ActionPanel/OutputDevicesActionButton.qml +++ b/resources/qml/ActionPanel/OutputDevicesActionButton.qml @@ -68,7 +68,7 @@ Item closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent - contentItem: Column + contentItem: ColumnLayout { Repeater { @@ -80,7 +80,7 @@ Item color: "transparent" cornerRadius: 0 hoverColor: UM.Theme.getColor("primary") - + Layout.fillWidth: true onClicked: { UM.OutputDeviceManager.setActiveDevice(model.id) From 098714254d8a5d366f22d1894083fba4fee4aaad Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 26 Nov 2018 13:35:02 +0100 Subject: [PATCH 090/146] Remove unused code CURA-5959 --- resources/qml/SaveButton.qml | 478 ----------------------------------- 1 file changed, 478 deletions(-) delete mode 100644 resources/qml/SaveButton.qml diff --git a/resources/qml/SaveButton.qml b/resources/qml/SaveButton.qml deleted file mode 100644 index c2d310e30c..0000000000 --- a/resources/qml/SaveButton.qml +++ /dev/null @@ -1,478 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Cura is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.7 -import QtQuick.Controls 1.1 -import QtQuick.Controls.Styles 1.1 -import QtQuick.Layouts 1.1 - -import UM 1.1 as UM -import Cura 1.0 as Cura - -// This widget does so much more than "just" being a save button, so it should be refactored at some point in time. -Item -{ - id: base; - UM.I18nCatalog { id: catalog; name: "cura"} - - property real progress: UM.Backend.progress - property int backendState: UM.Backend.state - property bool activity: CuraApplication.platformActivity - - property alias buttonRowWidth: saveRow.width - - property string fileBaseName - property string statusText: - { - if(!activity) - { - return catalog.i18nc("@label:PrintjobStatus", "Please load a 3D model"); - } - - switch(base.backendState) - { - case 1: - return catalog.i18nc("@label:PrintjobStatus", "Ready to slice"); - case 2: - return catalog.i18nc("@label:PrintjobStatus", "Slicing..."); - case 3: - return catalog.i18nc("@label:PrintjobStatus %1 is target operation", "Ready to %1").arg(UM.OutputDeviceManager.activeDeviceShortDescription); - case 4: - return catalog.i18nc("@label:PrintjobStatus", "Unable to Slice"); - case 5: - return catalog.i18nc("@label:PrintjobStatus", "Slicing unavailable"); - default: - return ""; - } - } - - function sliceOrStopSlicing() - { - try - { - if ([1, 5].indexOf(base.backendState) != -1) - { - CuraApplication.backend.forceSlice(); - } - else - { - CuraApplication.backend.stopSlicing(); - } - } - catch (e) - { - console.log("Could not start or stop slicing.", e) - } - } - - Label - { - id: statusLabel - width: parent.width - 2 * UM.Theme.getSize("thick_margin").width - anchors.top: parent.top - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("thick_margin").width - - color: UM.Theme.getColor("text") - font: UM.Theme.getFont("default_bold") - text: statusText; - } - - Rectangle - { - id: progressBar - width: parent.width - 2 * UM.Theme.getSize("thick_margin").width - height: UM.Theme.getSize("progressbar").height - anchors.top: statusLabel.bottom - anchors.topMargin: Math.round(UM.Theme.getSize("thick_margin").height / 4) - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("thick_margin").width - radius: UM.Theme.getSize("progressbar_radius").width - color: UM.Theme.getColor("progressbar_background") - - Rectangle - { - width: Math.max(parent.width * base.progress) - height: parent.height - color: UM.Theme.getColor("progressbar_control") - radius: UM.Theme.getSize("progressbar_radius").width - visible: base.backendState == 2 - } - } - - // Shortcut for "save as/print/..." - Action - { - shortcut: "Ctrl+P" - onTriggered: - { - // only work when the button is enabled - if (saveToButton.enabled) - { - saveToButton.clicked(); - } - // prepare button - if (prepareButton.enabled) - { - sliceOrStopSlicing(); - } - } - } - - Item - { - id: saveRow - width: { - // using childrenRect.width directly causes a binding loop, because setting the width affects the childrenRect - var children_width = UM.Theme.getSize("default_margin").width; - for (var index in children) - { - var child = children[index]; - if(child.visible) - { - children_width += child.width + child.anchors.rightMargin; - } - } - return Math.min(children_width, base.width - UM.Theme.getSize("thick_margin").width); - } - height: saveToButton.height - anchors.bottom: parent.bottom - anchors.bottomMargin: UM.Theme.getSize("thick_margin").height - anchors.right: parent.right - clip: true - - Row - { - id: additionalComponentsRow - anchors.top: parent.top - anchors.right: saveToButton.visible ? saveToButton.left : (prepareButton.visible ? prepareButton.left : parent.right) - anchors.rightMargin: UM.Theme.getSize("default_margin").width - - spacing: UM.Theme.getSize("default_margin").width - } - - Component.onCompleted: - { - saveRow.addAdditionalComponents("saveButton") - } - - Connections - { - target: CuraApplication - onAdditionalComponentsChanged: saveRow.addAdditionalComponents("saveButton") - } - - function addAdditionalComponents (areaId) - { - if(areaId == "saveButton") - { - for (var component in CuraApplication.additionalComponents["saveButton"]) - { - CuraApplication.additionalComponents["saveButton"][component].parent = additionalComponentsRow - } - } - } - - Connections - { - target: UM.Preferences - onPreferenceChanged: - { - var autoSlice = UM.Preferences.getValue("general/auto_slice"); - prepareButton.autoSlice = autoSlice; - saveToButton.autoSlice = autoSlice; - } - } - - // Prepare button, only shows if auto_slice is off - Button - { - id: prepareButton - - tooltip: [1, 5].indexOf(base.backendState) != -1 ? catalog.i18nc("@info:tooltip","Slice current printjob") : catalog.i18nc("@info:tooltip","Cancel slicing process") - // 1 = not started, 2 = Processing - enabled: ([1, 2].indexOf(base.backendState) != -1) && base.activity - visible: !autoSlice && ([1, 2, 4].indexOf(base.backendState) != -1) && base.activity - property bool autoSlice - height: UM.Theme.getSize("save_button_save_to_button").height - - anchors.top: parent.top - anchors.right: parent.right - anchors.rightMargin: UM.Theme.getSize("thick_margin").width - - // 1 = not started, 4 = error, 5 = disabled - text: [1, 4, 5].indexOf(base.backendState) != -1 ? catalog.i18nc("@label:Printjob", "Prepare") : catalog.i18nc("@label:Printjob", "Cancel") - onClicked: - { - sliceOrStopSlicing(); - } - - style: ButtonStyle - { - background: Rectangle - { - border.width: UM.Theme.getSize("default_lining").width - border.color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled_border"); - } - else if(control.pressed) - { - return UM.Theme.getColor("action_button_active_border"); - } - else if(control.hovered) - { - return UM.Theme.getColor("action_button_hovered_border"); - } - else - { - return UM.Theme.getColor("action_button_border"); - } - } - color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled"); - } - else if(control.pressed) - { - return UM.Theme.getColor("action_button_active"); - } - else if(control.hovered) - { - return UM.Theme.getColor("action_button_hovered"); - } - else - { - return UM.Theme.getColor("action_button"); - } - } - - Behavior on color { ColorAnimation { duration: 50; } } - - implicitWidth: actualLabel.contentWidth + (UM.Theme.getSize("thick_margin").width * 2) - - Label - { - id: actualLabel - anchors.centerIn: parent - color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled_text"); - } - else if(control.pressed) - { - return UM.Theme.getColor("action_button_active_text"); - } - else if(control.hovered) - { - return UM.Theme.getColor("action_button_hovered_text"); - } - else - { - return UM.Theme.getColor("action_button_text"); - } - } - font: UM.Theme.getFont("action_button") - text: control.text; - } - } - label: Item {} - } - } - - Button - { - id: saveToButton - - tooltip: UM.OutputDeviceManager.activeDeviceDescription; - // 3 = done, 5 = disabled - enabled: base.backendState != "undefined" && (base.backendState == 3 || base.backendState == 5) && base.activity == true - visible: base.backendState != "undefined" && autoSlice || ((base.backendState == 3 || base.backendState == 5) && base.activity == true) - property bool autoSlice - height: UM.Theme.getSize("save_button_save_to_button").height - - anchors.top: parent.top - anchors.right: deviceSelectionMenu.visible ? deviceSelectionMenu.left : parent.right - anchors.rightMargin: deviceSelectionMenu.visible ? -3 * UM.Theme.getSize("default_lining").width : UM.Theme.getSize("thick_margin").width - - text: UM.OutputDeviceManager.activeDeviceShortDescription - onClicked: - { - forceActiveFocus(); - UM.OutputDeviceManager.requestWriteToDevice(UM.OutputDeviceManager.activeDevice, PrintInformation.jobName, - { "filter_by_machine": true, "preferred_mimetypes": Cura.MachineManager.activeMachine.preferred_output_file_formats }); - } - - style: ButtonStyle - { - background: Rectangle - { - border.width: UM.Theme.getSize("default_lining").width - border.color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled_border"); - } - else if(control.pressed) - { - return UM.Theme.getColor("print_button_ready_pressed_border"); - } - else if(control.hovered) - { - return UM.Theme.getColor("print_button_ready_hovered_border"); - } - else - { - return UM.Theme.getColor("print_button_ready_border"); - } - } - color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled"); - } - else if(control.pressed) - { - return UM.Theme.getColor("print_button_ready_pressed"); - } - else if(control.hovered) - { - return UM.Theme.getColor("print_button_ready_hovered"); - } - else - { - return UM.Theme.getColor("print_button_ready"); - } - } - - Behavior on color { ColorAnimation { duration: 50; } } - - implicitWidth: actualLabel.contentWidth + (UM.Theme.getSize("thick_margin").width * 2) - - Label - { - id: actualLabel - anchors.centerIn: parent - color: control.enabled ? UM.Theme.getColor("print_button_ready_text") : UM.Theme.getColor("action_button_disabled_text") - font: UM.Theme.getFont("action_button") - text: control.text - } - } - label: Item { } - } - } - - Button - { - id: deviceSelectionMenu - tooltip: catalog.i18nc("@info:tooltip","Select the active output device"); - anchors.top: parent.top - anchors.right: parent.right - - anchors.rightMargin: UM.Theme.getSize("thick_margin").width - width: UM.Theme.getSize("save_button_save_to_button").height - height: UM.Theme.getSize("save_button_save_to_button").height - - // 3 = Done, 5 = Disabled - enabled: (base.backendState == 3 || base.backendState == 5) && base.activity == true - visible: (devicesModel.deviceCount > 1) && (base.backendState == 3 || base.backendState == 5) && base.activity == true - - - style: ButtonStyle - { - background: Rectangle - { - id: deviceSelectionIcon - border.width: UM.Theme.getSize("default_lining").width - border.color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled_border") - } - else if(control.pressed) - { - return UM.Theme.getColor("print_button_ready_pressed_border") - } - else if(control.hovered) - { - return UM.Theme.getColor("print_button_ready_hovered_border") - } - else - { - return UM.Theme.getColor("print_button_ready_border") - } - } - color: - { - if(!control.enabled) - { - return UM.Theme.getColor("action_button_disabled") - } - else if(control.pressed) - { - return UM.Theme.getColor("print_button_ready_pressed") - } - else if(control.hovered) - { - return UM.Theme.getColor("print_button_ready_hovered") - } - else - { - return UM.Theme.getColor("print_button_ready") - } - } - Behavior on color { ColorAnimation { duration: 50; } } - anchors.left: parent.left - anchors.leftMargin: Math.round(UM.Theme.getSize("save_button_text_margin").width / 2); - width: parent.height - height: parent.height - - UM.RecolorImage - { - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - width: UM.Theme.getSize("standard_arrow").width - height: UM.Theme.getSize("standard_arrow").height - sourceSize.width: width - sourceSize.height: height - color: control.enabled ? UM.Theme.getColor("print_button_ready_text") : UM.Theme.getColor("action_button_disabled_text") - source: UM.Theme.getIcon("arrow_bottom") - } - } - } - - menu: Menu - { - id: devicesMenu; - Instantiator - { - model: devicesModel; - MenuItem - { - text: model.description - checkable: true; - checked: model.id == UM.OutputDeviceManager.activeDevice - exclusiveGroup: devicesMenuGroup - onTriggered: - { - UM.OutputDeviceManager.setActiveDevice(model.id); - } - } - onObjectAdded: devicesMenu.insertItem(index, object) - onObjectRemoved: devicesMenu.removeItem(object) - } - ExclusiveGroup { id: devicesMenuGroup } - } - } - UM.OutputDevicesModel { id: devicesModel } - } -} From 1fe65013584b6bb1130a8fc4a7bac3e3ddff618c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 26 Nov 2018 13:35:41 +0100 Subject: [PATCH 091/146] Removed unused entries from theme --- resources/themes/cura-light/theme.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index cbdc37caa1..59927da663 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -181,14 +181,6 @@ "action_button_shadow": [64, 47, 205, 255], "action_button_disabled_shadow": [228, 228, 228, 255], - "print_button_ready": [50, 130, 255, 255], - "print_button_ready_border": [50, 130, 255, 255], - "print_button_ready_text": [255, 255, 255, 255], - "print_button_ready_hovered": [30, 186, 245, 243], - "print_button_ready_hovered_border": [30, 186, 245, 243], - "print_button_ready_pressed": [30, 186, 245, 243], - "print_button_ready_pressed_border": [30, 186, 245, 243], - "scrollbar_background": [255, 255, 255, 255], "scrollbar_handle": [31, 36, 39, 255], "scrollbar_handle_hover": [12, 159, 227, 255], From 3c86c0ae6c691c4eb1be17f0fe15b1ff1cc5919f Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 26 Nov 2018 13:36:45 +0100 Subject: [PATCH 092/146] Fix spacing CURA-5959 --- resources/themes/cura-light/theme.json | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 59927da663..001818c2f8 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -80,8 +80,7 @@ "thick_lining": [127, 127, 127, 255], "lining": [192, 193, 194, 255], "viewport_overlay": [0, 0, 0, 192], - - + "primary": [50, 130, 255, 255], "primary_shadow": [64, 47, 205, 255], "primary_hover": [48, 182, 231, 255], @@ -90,15 +89,15 @@ "secondary": [245, 245, 245, 255], "secondary_shadow": [228, 228, 228, 255], - "primary_button": [38,113,231,255], - "primary_button_shadow": [27,95,202, 255], - "primary_button_hover": [81,145,247, 255], + "primary_button": [38, 113, 231, 255], + "primary_button_shadow": [27, 95, 202, 255], + "primary_button_hover": [81, 145, 247, 255], "primary_button_text": [255, 255, 255, 255], - "secondary_button": [240,240,240, 255], - "secondary_button_shadow": [228, 228, 228, 255], - "secondary_button_hover": [228,228,228, 255], - "secondary_button_text": [30,102,215, 255], + "secondary_button": [240, 240, 240, 255], + "secondary_button_shadow": [228, 228, 228, 255], + "secondary_button_hover": [228, 228, 228, 255], + "secondary_button_text": [30, 102, 215, 255], "main_window_header_background": [10, 8, 80, 255], "main_window_header_button_text_active": [10, 8, 80, 255], From ebae4347a841e90f257d3296b24ca3e8531e6852 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Mon, 26 Nov 2018 13:44:32 +0100 Subject: [PATCH 093/146] Fix unit tests that were failing after adding the getAbbreviatedMachineName to the machine manager. Contributes to CURA-5942. --- tests/TestPrintInformation.py | 52 ++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/tests/TestPrintInformation.py b/tests/TestPrintInformation.py index a226a437c6..baa36fb338 100644 --- a/tests/TestPrintInformation.py +++ b/tests/TestPrintInformation.py @@ -1,5 +1,7 @@ +import functools from cura import PrintInformation +from cura.Settings.MachineManager import MachineManager from unittest.mock import MagicMock, patch from UM.Application import Application @@ -11,14 +13,20 @@ def getPrintInformation(printer_name) -> PrintInformation: mock_application = MagicMock() global_container_stack = MagicMock() - global_container_stack.definition.getName = MagicMock(return_value=printer_name) - mock_application.getGlobalContainerStack = MagicMock(return_value=global_container_stack) + global_container_stack.definition.getName = MagicMock(return_value = printer_name) + mock_application.getGlobalContainerStack = MagicMock(return_value = global_container_stack) - multiBuildPlateModel = MagicMock() - multiBuildPlateModel.maxBuildPlate = 0 - mock_application.getMultiBuildPlateModel = MagicMock(return_value=multiBuildPlateModel) + multi_build_plate_model = MagicMock() + multi_build_plate_model.maxBuildPlate = 0 + mock_application.getMultiBuildPlateModel = MagicMock(return_value = multi_build_plate_model) - Application.getInstance = MagicMock(return_type=mock_application) + # Mock-up the entire machine manager except the function that needs to be tested: getAbbreviatedMachineName + original_get_abbreviated_name = MachineManager.getAbbreviatedMachineName + mock_machine_manager = MagicMock() + mock_machine_manager.getAbbreviatedMachineName = functools.partial(original_get_abbreviated_name, mock_machine_manager) + mock_application.getMachineManager = MagicMock(return_value = mock_machine_manager) + + Application.getInstance = MagicMock(return_type = mock_application) with patch("json.loads", lambda x: {}): print_information = PrintInformation.PrintInformation(mock_application) @@ -28,17 +36,17 @@ def getPrintInformation(printer_name) -> PrintInformation: def setup_module(): MimeTypeDatabase.addMimeType( MimeType( - name="application/vnd.ms-package.3dmanufacturing-3dmodel+xml", - comment="3MF", - suffixes=["3mf"] + name = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml", + comment = "3MF", + suffixes = ["3mf"] ) ) MimeTypeDatabase.addMimeType( MimeType( - name="application/x-cura-gcode-file", - comment="Cura GCode File", - suffixes=["gcode"] + name = "application/x-cura-gcode-file", + comment = "Cura GCode File", + suffixes = ["gcode"] ) ) @@ -49,42 +57,42 @@ def test_setProjectName(): print_information = getPrintInformation("ultimaker") # Test simple name - project_name = ["HelloWorld",".3mf"] + project_name = ["HelloWorld", ".3mf"] print_information.setProjectName(project_name[0] + project_name[1]) assert "UM_" + project_name[0] == print_information._job_name # Test the name with one dot - project_name = ["Hello.World",".3mf"] + project_name = ["Hello.World", ".3mf"] print_information.setProjectName(project_name[0] + project_name[1]) assert "UM_" + project_name[0] == print_information._job_name # Test the name with two dot - project_name = ["Hello.World.World",".3mf"] + project_name = ["Hello.World.World", ".3mf"] print_information.setProjectName(project_name[0] + project_name[1]) assert "UM_" + project_name[0] == print_information._job_name # Test the name with dot at the beginning - project_name = [".Hello.World",".3mf"] + project_name = [".Hello.World", ".3mf"] print_information.setProjectName(project_name[0] + project_name[1]) assert "UM_" + project_name[0] == print_information._job_name # Test the name with underline - project_name = ["Hello_World",".3mf"] + project_name = ["Hello_World", ".3mf"] print_information.setProjectName(project_name[0] + project_name[1]) assert "UM_" + project_name[0] == print_information._job_name # Test gcode extension - project_name = ["Hello_World",".gcode"] + project_name = ["Hello_World", ".gcode"] print_information.setProjectName(project_name[0] + project_name[1]) assert "UM_" + project_name[0] == print_information._job_name # Test empty project name - project_name = ["",""] + project_name = ["", ""] print_information.setProjectName(project_name[0] + project_name[1]) assert print_information.UNTITLED_JOB_NAME == print_information._job_name # Test wrong file extension - project_name = ["Hello_World",".test"] + project_name = ["Hello_World", ".test"] print_information.setProjectName(project_name[0] + project_name[1]) assert "UM_" + project_name[0] != print_information._job_name @@ -93,7 +101,7 @@ def test_setJobName(): print_information = getPrintInformation("ultimaker") print_information._abbr_machine = "UM" - print_information.setJobName("UM_HelloWorld", is_user_specified_job_name=False) + print_information.setJobName("UM_HelloWorld", is_user_specified_job_name = False) def test_defineAbbreviatedMachineName(): @@ -102,6 +110,6 @@ def test_defineAbbreviatedMachineName(): print_information = getPrintInformation(printer_name) # Test not ultimaker printer, name suffix should have first letter from the printer name - project_name = ["HelloWorld",".3mf"] + project_name = ["HelloWorld", ".3mf"] print_information.setProjectName(project_name[0] + project_name[1]) assert printer_name[0] + "_" + project_name[0] == print_information._job_name \ No newline at end of file From a6544997525f0a066429223da1bd432eb3653ec5 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 26 Nov 2018 13:47:22 +0100 Subject: [PATCH 094/146] Re-enable hover for marketplace button CURA-5959 --- plugins/Toolbox/resources/qml/ToolboxDetailPage.qml | 2 +- resources/qml/MainWindow/MainWindowHeader.qml | 6 ++++-- resources/themes/cura-light/theme.json | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml b/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml index 437a2ef351..0c04dc2bab 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml @@ -37,7 +37,7 @@ Item leftMargin: UM.Theme.getSize("wide_margin").width topMargin: UM.Theme.getSize("wide_margin").height } - color: white //Always a white background for image (regardless of theme). + color: "white" //Always a white background for image (regardless of theme). Image { anchors.fill: parent diff --git a/resources/qml/MainWindow/MainWindowHeader.qml b/resources/qml/MainWindow/MainWindowHeader.qml index ceb27dd726..a24af7ee45 100644 --- a/resources/qml/MainWindow/MainWindowHeader.qml +++ b/resources/qml/MainWindow/MainWindowHeader.qml @@ -81,10 +81,12 @@ Rectangle height: Math.round(0.5 * UM.Theme.getSize("main_window_header").height) onClicked: Cura.Actions.browsePackages.trigger() + hoverEnabled: true + background: Rectangle { radius: UM.Theme.getSize("action_button_radius").width - color: "transparent" + color: marketplaceButton.hovered ? UM.Theme.getColor("primary_text") : UM.Theme.getColor("main_window_header_background") border.width: UM.Theme.getSize("default_lining").width border.color: UM.Theme.getColor("primary_text") } @@ -93,7 +95,7 @@ Rectangle { id: label text: marketplaceButton.text - color: UM.Theme.getColor("primary_text") + color: marketplaceButton.hovered ? UM.Theme.getColor("main_window_header_background") : UM.Theme.getColor("primary_text") width: contentWidth } diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 001818c2f8..2343cd3f2a 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -80,7 +80,7 @@ "thick_lining": [127, 127, 127, 255], "lining": [192, 193, 194, 255], "viewport_overlay": [0, 0, 0, 192], - + "primary": [50, 130, 255, 255], "primary_shadow": [64, 47, 205, 255], "primary_hover": [48, 182, 231, 255], From b63c4f7a74a06f6b167f6c48d3538b9566fc2e9f Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Mon, 26 Nov 2018 14:06:34 +0100 Subject: [PATCH 095/146] Add header in new files. Remove unused imports. Contributes to CURA-5959. --- resources/qml/ActionButton.qml | 1 - resources/qml/PrimaryButton.qml | 4 +++- resources/qml/SecondaryButton.qml | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/resources/qml/ActionButton.qml b/resources/qml/ActionButton.qml index 6dd5839bb9..b9a04f3b46 100644 --- a/resources/qml/ActionButton.qml +++ b/resources/qml/ActionButton.qml @@ -3,7 +3,6 @@ import QtQuick 2.7 import QtQuick.Controls 2.1 -import QtQuick.Layouts 1.3 import QtGraphicalEffects 1.0 // For the dropshadow diff --git a/resources/qml/PrimaryButton.qml b/resources/qml/PrimaryButton.qml index 8450e524e2..fca63d2cdb 100644 --- a/resources/qml/PrimaryButton.qml +++ b/resources/qml/PrimaryButton.qml @@ -1,5 +1,7 @@ +// Copyright (c) 2018 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + import QtQuick 2.2 -import QtQuick.Controls 1.1 import UM 1.4 as UM import Cura 1.1 as Cura diff --git a/resources/qml/SecondaryButton.qml b/resources/qml/SecondaryButton.qml index 0e6b79b3a7..f03d8acdfa 100644 --- a/resources/qml/SecondaryButton.qml +++ b/resources/qml/SecondaryButton.qml @@ -1,5 +1,7 @@ +// Copyright (c) 2018 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + import QtQuick 2.2 -import QtQuick.Controls 1.1 import UM 1.4 as UM import Cura 1.1 as Cura From 68a90ec510b15d41ba585f013c7d9c5fe52fb94f Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 26 Nov 2018 14:08:21 +0100 Subject: [PATCH 096/146] Use simple models instead of namedtuples Named tuples would throw a TypeError if an unknown attribute was set, but we just want to ignore those --- plugins/UM3NetworkPrinting/src/Models.py | 67 ++++++++++++++---------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Models.py b/plugins/UM3NetworkPrinting/src/Models.py index bcdeb8299c..e2ad411e90 100644 --- a/plugins/UM3NetworkPrinting/src/Models.py +++ b/plugins/UM3NetworkPrinting/src/Models.py @@ -1,33 +1,42 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from collections import namedtuple -ClusterMaterial = namedtuple("ClusterMaterial", [ - "guid", # Type: str - "material", # Type: str - "brand", # Type: str - "version", # Type: int - "color", # Type: str - "density" # Type: str -]) -LocalMaterial = namedtuple("LocalMaterial", [ - "GUID", # Type: str - "id", # Type: str - "type", # Type: str - "status", # Type: str - "base_file", # Type: str - "setting_version", # Type: int - "version", # Type: int - "name", # Type: str - "brand", # Type: str - "material", # Type: str - "color_name", # Type: str - "color_code", # Type: str - "description", # Type: str - "adhesion_info", # Type: str - "approximate_diameter", # Type: str - "properties", # Type: str - "definition", # Type: str - "compatible" # Type: str -]) +## Base model that maps kwargs to instance attributes. +class BaseModel: + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + +## Class representing a material that was fetched from the cluster API. +class ClusterMaterial(BaseModel): + def __init__(self, **kwargs): + self.guid = None # type: str + self.material = None # type: str + self.brand = None # type: str + self.version = None # type: int + self.color = None # type: str + self.density = None # type: str + super().__init__(**kwargs) + + +## Class representing a local material that was fetched from the container registry. +class LocalMaterial(BaseModel): + def __init__(self, **kwargs): + self.GUID = None # type: str + self.id = None # type: str + self.type = None # type: str + self.status = None # type: str + self.base_file = None # type: str + self.setting_version = None # type: int + self.version = None # type: int + self.brand = None # type: str + self.material = None # type: str + self.color_name = None # type: str + self.color_code = None # type: str + self.description = None # type: str + self.adhesion_info = None # type: str + self.approximate_diameter = None # type: str + self.definition = None # type: str + self.compatible = None # type: bool + super().__init__(**kwargs) From 12f78fa21ad022d9f1f278fb5f3d56ced7e65f60 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Mon, 26 Nov 2018 14:29:42 +0100 Subject: [PATCH 097/146] Remove border of the popup selector for the output devices. Contributes to CURA-5959. --- resources/qml/ActionPanel/OutputDevicesActionButton.qml | 3 --- 1 file changed, 3 deletions(-) diff --git a/resources/qml/ActionPanel/OutputDevicesActionButton.qml b/resources/qml/ActionPanel/OutputDevicesActionButton.qml index e4b4884794..2111038cfc 100644 --- a/resources/qml/ActionPanel/OutputDevicesActionButton.qml +++ b/resources/qml/ActionPanel/OutputDevicesActionButton.qml @@ -94,10 +94,7 @@ Item { opacity: visible ? 1 : 0 Behavior on opacity { NumberAnimation { duration: 100 } } - radius: UM.Theme.getSize("default_radius").width color: UM.Theme.getColor("action_panel_secondary") - border.color: UM.Theme.getColor("lining") - border.width: UM.Theme.getSize("default_lining").width } } } From a382b77eaa81998d5a413a019701137117b45eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marijn=20De=C3=A9?= Date: Mon, 26 Nov 2018 14:30:17 +0100 Subject: [PATCH 098/146] Added validation to the models --- plugins/UM3NetworkPrinting/src/Models.py | 14 ++++++++++++++ plugins/UM3NetworkPrinting/src/SendMaterialJob.py | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Models.py b/plugins/UM3NetworkPrinting/src/Models.py index e2ad411e90..d0708c8127 100644 --- a/plugins/UM3NetworkPrinting/src/Models.py +++ b/plugins/UM3NetworkPrinting/src/Models.py @@ -6,6 +6,10 @@ class BaseModel: def __init__(self, **kwargs): self.__dict__.update(kwargs) + self.validate() + + def validate(self): + pass ## Class representing a material that was fetched from the cluster API. @@ -19,6 +23,10 @@ class ClusterMaterial(BaseModel): self.density = None # type: str super().__init__(**kwargs) + def validate(self): + if not self.guid: + raise ValueError("guid is required on ClusterMaterial") + ## Class representing a local material that was fetched from the container registry. class LocalMaterial(BaseModel): @@ -40,3 +48,9 @@ class LocalMaterial(BaseModel): self.definition = None # type: str self.compatible = None # type: bool super().__init__(**kwargs) + + def validate(self): + if not self.GUID: + raise ValueError("guid is required on LocalMaterial") + if not self.id: + raise ValueError("id is required on LocalMaterial") diff --git a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py index 48760af28e..6f33e75ee1 100644 --- a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py @@ -159,8 +159,8 @@ class SendMaterialJob(Job): Logger.log("e", "Request material storage on printer: I didn't understand the printer's answer.") except json.JSONDecodeError: Logger.log("e", "Request material storage on printer: I didn't understand the printer's answer.") - except TypeError: - Logger.log("e", "Request material storage on printer: Printer's answer was missing GUIDs.") + except ValueError: + Logger.log("e", "Request material storage on printer: Printer's answer was missing a value.") ## Retrieves a list of local materials # From 0a1c0e18d1bca499168b062f1e0538665768cd85 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Mon, 26 Nov 2018 14:38:43 +0100 Subject: [PATCH 099/146] Reuse the component SecondaryButton in the printer selector. Contributes to CURA-5942. --- resources/qml/PrinterSelector/MachineSelector.qml | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/resources/qml/PrinterSelector/MachineSelector.qml b/resources/qml/PrinterSelector/MachineSelector.qml index 120ce02edd..93e5103aa8 100644 --- a/resources/qml/PrinterSelector/MachineSelector.qml +++ b/resources/qml/PrinterSelector/MachineSelector.qml @@ -125,15 +125,11 @@ Cura.ExpandableComponent padding: UM.Theme.getSize("default_margin").width spacing: UM.Theme.getSize("default_margin").width - Cura.ActionButton + Cura.SecondaryButton { leftPadding: UM.Theme.getSize("default_margin").width rightPadding: UM.Theme.getSize("default_margin").width text: catalog.i18nc("@button", "Add printer") - color: UM.Theme.getColor("secondary") - hoverColor: UM.Theme.getColor("secondary") - textColor: UM.Theme.getColor("primary") - textHoverColor: UM.Theme.getColor("text") onClicked: { togglePopup() @@ -141,15 +137,11 @@ Cura.ExpandableComponent } } - Cura.ActionButton + Cura.SecondaryButton { leftPadding: UM.Theme.getSize("default_margin").width rightPadding: UM.Theme.getSize("default_margin").width text: catalog.i18nc("@button", "Manage printers") - color: UM.Theme.getColor("secondary") - hoverColor: UM.Theme.getColor("secondary") - textColor: UM.Theme.getColor("primary") - textHoverColor: UM.Theme.getColor("text") onClicked: { togglePopup() From 84f263f1111a58d46817c2d9d6572e7676a8dcf3 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Mon, 26 Nov 2018 15:08:33 +0100 Subject: [PATCH 100/146] Fix style for the open file button in the prepare menu. Contributes to CURA-5942. --- plugins/PrepareStage/PrepareMenu.qml | 41 ++++++++++++++++------------ 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/plugins/PrepareStage/PrepareMenu.qml b/plugins/PrepareStage/PrepareMenu.qml index a99426acd8..a953c2b5d1 100644 --- a/plugins/PrepareStage/PrepareMenu.qml +++ b/plugins/PrepareStage/PrepareMenu.qml @@ -3,7 +3,7 @@ import QtQuick 2.7 import QtQuick.Layouts 1.1 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import UM 1.3 as UM import Cura 1.1 as Cura @@ -27,14 +27,14 @@ Item Item { anchors.horizontalCenter: parent.horizontalCenter - width: openFileButtonBackground.width + itemRow.width + UM.Theme.getSize("default_margin").width + width: openFileButton.width + itemRow.width + UM.Theme.getSize("default_margin").width height: parent.height RowLayout { id: itemRow - anchors.left: openFileButtonBackground.right + anchors.left: openFileButton.right anchors.leftMargin: UM.Theme.getSize("default_margin").width width: Math.round(0.9 * prepareMenu.width) @@ -44,7 +44,7 @@ Item Cura.MachineSelector { id: machineSelection - z: openFileButtonBackground.z - 1 //Ensure that the tooltip of the open file button stays above the item row. + z: openFileButton.z - 1 //Ensure that the tooltip of the open file button stays above the item row. headerCornerSide: Cura.RoundedRectangle.Direction.Left Layout.minimumWidth: UM.Theme.getSize("machine_selector_widget").width Layout.maximumWidth: UM.Theme.getSize("machine_selector_widget").width @@ -86,24 +86,31 @@ Item } } - Rectangle + Button { - id: openFileButtonBackground + id: openFileButton height: UM.Theme.getSize("stage_menu").height width: UM.Theme.getSize("stage_menu").height + onClicked: Cura.Actions.open.trigger() - radius: UM.Theme.getSize("default_radius").width - color: UM.Theme.getColor("toolbar_background") - - Button + contentItem: UM.RecolorImage { - id: openFileButton - text: catalog.i18nc("@action:button", "Open File") - iconSource: UM.Theme.getIcon("load") - style: UM.Theme.styles.toolbar_button - tooltip: "" - action: Cura.Actions.open - anchors.centerIn: parent + id: buttonIcon + source: UM.Theme.getIcon("load") + width: UM.Theme.getSize("button_icon").width + height: UM.Theme.getSize("button_icon").height + color: UM.Theme.getColor("toolbar_button_text") + + sourceSize: UM.Theme.getSize("button_icon") + } + + background: Rectangle + { + height: UM.Theme.getSize("stage_menu").height + width: UM.Theme.getSize("stage_menu").height + + radius: UM.Theme.getSize("default_radius").width + color: openFileButton.hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("action_button") } } } From 9732099250f9085137883907293f5dbd1080691a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 26 Nov 2018 15:14:32 +0100 Subject: [PATCH 101/146] Add border to rounded rectangle. It's designed so that it works in exactly the same way as rectangle. --- resources/qml/BorderGroup.qml | 7 +++++++ resources/qml/RoundedRectangle.qml | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 resources/qml/BorderGroup.qml diff --git a/resources/qml/BorderGroup.qml b/resources/qml/BorderGroup.qml new file mode 100644 index 0000000000..94d0d68594 --- /dev/null +++ b/resources/qml/BorderGroup.qml @@ -0,0 +1,7 @@ +import QtQuick 2.0 + +QtObject +{ + property real width: 0 + property color color: "black" +} diff --git a/resources/qml/RoundedRectangle.qml b/resources/qml/RoundedRectangle.qml index 9ad2230be5..3ca05e2125 100644 --- a/resources/qml/RoundedRectangle.qml +++ b/resources/qml/RoundedRectangle.qml @@ -5,6 +5,7 @@ import UM 1.2 as UM // The rounded rectangle works mostly like a regular rectangle, but provides the option to have rounded corners on only one side of the rectangle. Item { + id: roundedRectangle // As per the regular rectangle property color color: "transparent" @@ -15,6 +16,9 @@ Item // 1 is down, 2 is left, 3 is up and 4 is right. property int cornerSide: RoundedRectangle.Direction.None + // Simple object to ensure that border.width and border.color work + property BorderGroup border: BorderGroup {} + enum Direction { None = 0, @@ -31,6 +35,8 @@ Item anchors.fill: parent radius: cornerSide != RoundedRectangle.Direction.None ? parent.radius : 0 color: parent.color + border.width: parent.border.width + border.color: parent.border.color } // The item that covers 2 of the corners to make them not rounded. @@ -45,5 +51,22 @@ Item right: cornerSide == RoundedRectangle.Direction.Left ? parent.right: undefined bottom: cornerSide == RoundedRectangle.Direction.Up ? parent.bottom: undefined } + + border.width: parent.border.width + border.color: parent.border.color + + Rectangle + { + color: roundedRectangle.color + height: cornerSide % 2 ? roundedRectangle.border.width: roundedRectangle.height - 2 * roundedRectangle.border.width + width: cornerSide % 2 ? roundedRectangle.width - 2 * roundedRectangle.border.width: roundedRectangle.border.width + anchors + { + right: cornerSide == RoundedRectangle.Direction.Right ? parent.right : undefined + bottom: cornerSide == RoundedRectangle.Direction.Down ? parent.bottom: undefined + horizontalCenter: cornerSide % 2 ? parent.horizontalCenter: undefined + verticalCenter: cornerSide % 2 ? undefined: parent.verticalCenter + } + } } } From da5683c876682019fe2360cf56fd27afe9f39844 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 26 Nov 2018 15:30:30 +0100 Subject: [PATCH 102/146] add typing to models --- plugins/UM3NetworkPrinting/src/Models.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Models.py b/plugins/UM3NetworkPrinting/src/Models.py index d0708c8127..2a34e41f86 100644 --- a/plugins/UM3NetworkPrinting/src/Models.py +++ b/plugins/UM3NetworkPrinting/src/Models.py @@ -4,17 +4,17 @@ ## Base model that maps kwargs to instance attributes. class BaseModel: - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: self.__dict__.update(kwargs) self.validate() - def validate(self): + def validate(self) -> None: pass ## Class representing a material that was fetched from the cluster API. class ClusterMaterial(BaseModel): - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: self.guid = None # type: str self.material = None # type: str self.brand = None # type: str @@ -23,14 +23,14 @@ class ClusterMaterial(BaseModel): self.density = None # type: str super().__init__(**kwargs) - def validate(self): + def validate(self) -> None: if not self.guid: raise ValueError("guid is required on ClusterMaterial") ## Class representing a local material that was fetched from the container registry. class LocalMaterial(BaseModel): - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: self.GUID = None # type: str self.id = None # type: str self.type = None # type: str @@ -49,7 +49,7 @@ class LocalMaterial(BaseModel): self.compatible = None # type: bool super().__init__(**kwargs) - def validate(self): + def validate(self) -> None: if not self.GUID: raise ValueError("guid is required on LocalMaterial") if not self.id: From 9afc5748a8ec1eb7bbbdb8ba7395f7b3d2554a49 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Mon, 26 Nov 2018 15:39:34 +0100 Subject: [PATCH 103/146] Add copyright header. --- resources/qml/BorderGroup.qml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/qml/BorderGroup.qml b/resources/qml/BorderGroup.qml index 94d0d68594..38ad9fadff 100644 --- a/resources/qml/BorderGroup.qml +++ b/resources/qml/BorderGroup.qml @@ -1,3 +1,6 @@ +// Copyright (c) 2018 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + import QtQuick 2.0 QtObject From 3d1157522a4d9c318a2c2d3f068c3326e3e3b81c Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Mon, 26 Nov 2018 15:51:36 +0100 Subject: [PATCH 104/146] Reuse the RoundedRectangle component and indicate that only the bottom part of the popup should be rounded. Contributes to CURA-5942. --- resources/qml/ExpandableComponent.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/qml/ExpandableComponent.qml b/resources/qml/ExpandableComponent.qml index ccfb9c6da2..9b2826daed 100644 --- a/resources/qml/ExpandableComponent.qml +++ b/resources/qml/ExpandableComponent.qml @@ -2,6 +2,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.3 import UM 1.2 as UM +import Cura 1.0 as Cura // The expandable component has 3 major sub components: // * The headerItem; Always visible and should hold some info about what happens if the component is expanded @@ -162,8 +163,9 @@ Item padding: UM.Theme.getSize("default_margin").width closePolicy: Popup.CloseOnPressOutsideParent - background: Rectangle + background: Cura.RoundedRectangle { + cornerSide: Cura.RoundedRectangle.Direction.Down color: popupBackgroundColor border.width: UM.Theme.getSize("default_lining").width border.color: UM.Theme.getColor("lining") From e4d8fb36abccd5e1a5f7f68bef086ae82ec3b9a4 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 26 Nov 2018 15:53:04 +0100 Subject: [PATCH 105/146] Add more typing as per request from @sedwards2009 --- plugins/UM3NetworkPrinting/src/Models.py | 35 ++++++++---------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Models.py b/plugins/UM3NetworkPrinting/src/Models.py index 2a34e41f86..5ef44bc006 100644 --- a/plugins/UM3NetworkPrinting/src/Models.py +++ b/plugins/UM3NetworkPrinting/src/Models.py @@ -14,43 +14,30 @@ class BaseModel: ## Class representing a material that was fetched from the cluster API. class ClusterMaterial(BaseModel): - def __init__(self, **kwargs) -> None: - self.guid = None # type: str - self.material = None # type: str - self.brand = None # type: str - self.version = None # type: int - self.color = None # type: str - self.density = None # type: str + def __init__(self, guid = str, version = str, **kwargs) -> None: + self.guid = guid # type: str + self.version = version # type: int super().__init__(**kwargs) def validate(self) -> None: if not self.guid: raise ValueError("guid is required on ClusterMaterial") + if not self.version: + raise ValueError("version is required on ClusterMaterial") ## Class representing a local material that was fetched from the container registry. class LocalMaterial(BaseModel): - def __init__(self, **kwargs) -> None: - self.GUID = None # type: str - self.id = None # type: str - self.type = None # type: str - self.status = None # type: str - self.base_file = None # type: str - self.setting_version = None # type: int - self.version = None # type: int - self.brand = None # type: str - self.material = None # type: str - self.color_name = None # type: str - self.color_code = None # type: str - self.description = None # type: str - self.adhesion_info = None # type: str - self.approximate_diameter = None # type: str - self.definition = None # type: str - self.compatible = None # type: bool + def __init__(self, GUID = str, id = str, version = str, **kwargs) -> None: + self.GUID = GUID # type: str + self.id = id # type: str + self.version = version # type: int super().__init__(**kwargs) def validate(self) -> None: if not self.GUID: raise ValueError("guid is required on LocalMaterial") + if not self.version: + raise ValueError("version is required on LocalMaterial") if not self.id: raise ValueError("id is required on LocalMaterial") From 6506596eceeb241decdc8456f8d2d3d21a9b140e Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 26 Nov 2018 16:19:01 +0100 Subject: [PATCH 106/146] Fix typing syntax --- plugins/UM3NetworkPrinting/src/Models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Models.py b/plugins/UM3NetworkPrinting/src/Models.py index 5ef44bc006..2bcac70766 100644 --- a/plugins/UM3NetworkPrinting/src/Models.py +++ b/plugins/UM3NetworkPrinting/src/Models.py @@ -14,7 +14,7 @@ class BaseModel: ## Class representing a material that was fetched from the cluster API. class ClusterMaterial(BaseModel): - def __init__(self, guid = str, version = str, **kwargs) -> None: + def __init__(self, guid: str, version: int, **kwargs) -> None: self.guid = guid # type: str self.version = version # type: int super().__init__(**kwargs) @@ -28,7 +28,7 @@ class ClusterMaterial(BaseModel): ## Class representing a local material that was fetched from the container registry. class LocalMaterial(BaseModel): - def __init__(self, GUID = str, id = str, version = str, **kwargs) -> None: + def __init__(self, GUID: str, id: str, version: int, **kwargs) -> None: self.GUID = GUID # type: str self.id = id # type: str self.version = version # type: int From efd5f3799bdb1c156722046bc2056b961d217f4d Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 26 Nov 2018 16:31:32 +0100 Subject: [PATCH 107/146] Also catch TypeError now that we have explicit arguments --- plugins/UM3NetworkPrinting/src/SendMaterialJob.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py index 6f33e75ee1..f536fad49a 100644 --- a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py @@ -160,7 +160,9 @@ class SendMaterialJob(Job): except json.JSONDecodeError: Logger.log("e", "Request material storage on printer: I didn't understand the printer's answer.") except ValueError: - Logger.log("e", "Request material storage on printer: Printer's answer was missing a value.") + Logger.log("e", "Request material storage on printer: Printer's answer had an incorrect value.") + except TypeError: + Logger.log("e", "Request material storage on printer: Printer's answer was missing a required value.") ## Retrieves a list of local materials # @@ -189,5 +191,7 @@ class SendMaterialJob(Job): Logger.logException("w", "Local material {} has missing values.".format(material["id"])) except ValueError: Logger.logException("w", "Local material {} has invalid values.".format(material["id"])) + except TypeError: + Logger.logException("w", "Local material {} has invalid values.".format(material["id"])) return result From d85aee1c53cb27983f73d153685a1681efa78b58 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 26 Nov 2018 16:40:29 +0100 Subject: [PATCH 108/146] Ensure that no weird data is set in the printSetupSelector on first start CURA-5961 --- resources/qml/PrintSetupSelector.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/qml/PrintSetupSelector.qml b/resources/qml/PrintSetupSelector.qml index 2ecdc9e546..9b90d8589f 100644 --- a/resources/qml/PrintSetupSelector.qml +++ b/resources/qml/PrintSetupSelector.qml @@ -54,12 +54,12 @@ Cura.ExpandableComponent IconWithText { source: UM.Theme.getIcon("category_layer_height") - text: Cura.MachineManager.activeQualityOrQualityChangesName + " " + layerHeight.properties.value + "mm" + text: Cura.MachineManager.activeStack ? Cura.MachineManager.activeQualityOrQualityChangesName + " " + layerHeight.properties.value + "mm" : "" UM.SettingPropertyProvider { id: layerHeight - containerStackId: Cura.MachineManager.activeStackId + containerStack: Cura.MachineManager.activeStack key: "layer_height" watchedProperties: ["value"] } @@ -68,12 +68,12 @@ Cura.ExpandableComponent IconWithText { source: UM.Theme.getIcon("category_infill") - text: parseInt(infillDensity.properties.value) + "%" + text: Cura.MachineManager.activeStack ? parseInt(infillDensity.properties.value) + "%" : "0%" UM.SettingPropertyProvider { id: infillDensity - containerStackId: Cura.MachineManager.activeStackId + containerStack: Cura.MachineManager.activeStack key: "infill_sparse_density" watchedProperties: ["value"] } From a825daea9565cdb5cfe405a1bb58750a76cdf988 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 26 Nov 2018 17:10:48 +0100 Subject: [PATCH 109/146] Cleanup the Theme.json There were a lot of sizes that weren't used --- resources/qml/Dialogs/AddMachineDialog.qml | 1 - resources/themes/cura-dark/theme.json | 4 - resources/themes/cura-light/styles.qml | 248 --------------------- resources/themes/cura-light/theme.json | 20 -- 4 files changed, 273 deletions(-) diff --git a/resources/qml/Dialogs/AddMachineDialog.qml b/resources/qml/Dialogs/AddMachineDialog.qml index aa160acd4d..8b2b9d1868 100644 --- a/resources/qml/Dialogs/AddMachineDialog.qml +++ b/resources/qml/Dialogs/AddMachineDialog.qml @@ -298,7 +298,6 @@ UM.Dialog id: machineName text: getMachineName() width: Math.floor(parent.width * 0.75) - implicitWidth: UM.Theme.getSize("standard_list_input").width maximumLength: 40 //validator: Cura.MachineNameValidator { } //TODO: Gives a segfault in PyQt5.6. For now, we must use a signal on text changed. validator: RegExpValidator diff --git a/resources/themes/cura-dark/theme.json b/resources/themes/cura-dark/theme.json index 62b1dbfa0f..34b944b25b 100644 --- a/resources/themes/cura-dark/theme.json +++ b/resources/themes/cura-dark/theme.json @@ -133,7 +133,6 @@ "slider_groove_border": [127, 127, 127, 255], "slider_groove_fill": [245, 245, 245, 255], "slider_handle": [255, 255, 255, 255], - "slider_handle_hover": [77, 182, 226, 255], "slider_handle_active": [68, 192, 255, 255], "slider_text_background": [255, 255, 255, 255], @@ -209,9 +208,6 @@ "quality_slider_unavailable": [179, 179, 179, 255], "quality_slider_available": [255, 255, 255, 255], - "quality_slider_handle": [255, 255, 255, 255], - "quality_slider_handle_hover": [127, 127, 127, 255], - "quality_slider_text": [255, 255, 255, 255], "toolbox_header_button_text_active": [255, 255, 255, 255], "toolbox_header_button_text_inactive": [128, 128, 128, 255], diff --git a/resources/themes/cura-light/styles.qml b/resources/themes/cura-light/styles.qml index 723b393efa..f4aeb95bb3 100755 --- a/resources/themes/cura-light/styles.qml +++ b/resources/themes/cura-light/styles.qml @@ -599,198 +599,6 @@ QtObject } } - property Component sidebar_category: Component - { - ButtonStyle - { - background: Rectangle - { - anchors.fill: parent - anchors.left: parent.left - anchors.leftMargin: Theme.getSize("thick_margin").width - anchors.right: parent.right - anchors.rightMargin: Theme.getSize("thick_margin").width - implicitHeight: Theme.getSize("section").height - color: - { - if(control.color) - { - return control.color; - } - else if(!control.enabled) - { - return Theme.getColor("setting_category_disabled"); - } - else if(control.hovered && control.checkable && control.checked) - { - return Theme.getColor("setting_category_active_hover"); - } - else if(control.pressed || (control.checkable && control.checked)) - { - return Theme.getColor("setting_category_active"); - } - else if(control.hovered) - { - return Theme.getColor("setting_category_hover"); - } - else - { - return Theme.getColor("setting_category"); - } - } - Behavior on color { ColorAnimation { duration: 50; } } - Rectangle - { - height: Theme.getSize("default_lining").height - width: parent.width - anchors.bottom: parent.bottom - color: - { - if(!control.enabled) - { - return Theme.getColor("setting_category_disabled_border"); - } - else if((control.hovered || control.activeFocus) && control.checkable && control.checked) - { - return Theme.getColor("setting_category_active_hover_border"); - } - else if(control.pressed || (control.checkable && control.checked)) - { - return Theme.getColor("setting_category_active_border"); - } - else if(control.hovered || control.activeFocus) - { - return Theme.getColor("setting_category_hover_border"); - } - else - { - return Theme.getColor("setting_category_border"); - } - } - } - } - label: Item - { - anchors.fill: parent - anchors.left: parent.left - Item - { - id: icon - anchors.left: parent.left - height: parent.height - width: Theme.getSize("section_icon_column").width - UM.RecolorImage - { - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: Theme.getSize("thick_margin").width - color: - { - if(!control.enabled) - { - return Theme.getColor("setting_category_disabled_text"); - } - else if((control.hovered || control.activeFocus) && control.checkable && control.checked) - { - return Theme.getColor("setting_category_active_hover_text"); - } - else if(control.pressed || (control.checkable && control.checked)) - { - return Theme.getColor("setting_category_active_text"); - } - else if(control.hovered || control.activeFocus) - { - return Theme.getColor("setting_category_hover_text"); - } - else - { - return Theme.getColor("setting_category_text"); - } - } - source: control.iconSource; - width: Theme.getSize("section_icon").width; - height: Theme.getSize("section_icon").height; - sourceSize.width: width + 15 * screenScaleFactor - sourceSize.height: width + 15 * screenScaleFactor - } - } - - Label - { - anchors - { - left: icon.right - leftMargin: Theme.getSize("default_margin").width - right: parent.right - verticalCenter: parent.verticalCenter - } - text: control.text - font: Theme.getFont("setting_category") - color: - { - if(!control.enabled) - { - return Theme.getColor("setting_category_disabled_text"); - } - else if((control.hovered || control.activeFocus) && control.checkable && control.checked) - { - return Theme.getColor("setting_category_active_hover_text"); - } - else if(control.pressed || (control.checkable && control.checked)) - { - return Theme.getColor("setting_category_active_text"); - } - else if(control.hovered || control.activeFocus) - { - return Theme.getColor("setting_category_hover_text"); - } - else - { - return Theme.getColor("setting_category_text"); - } - } - fontSizeMode: Text.HorizontalFit - minimumPointSize: 8 - } - UM.RecolorImage - { - id: category_arrow - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - anchors.rightMargin: Theme.getSize("default_margin").width * 3 - Math.round(width / 2) - width: Theme.getSize("standard_arrow").width - height: Theme.getSize("standard_arrow").height - sourceSize.width: width - sourceSize.height: width - color: - { - if(!control.enabled) - { - return Theme.getColor("setting_category_disabled_text"); - } - else if((control.hovered || control.activeFocus) && control.checkable && control.checked) - { - return Theme.getColor("setting_category_active_hover_text"); - } - else if(control.pressed || (control.checkable && control.checked)) - { - return Theme.getColor("setting_category_active_text"); - } - else if(control.hovered || control.activeFocus) - { - return Theme.getColor("setting_category_hover_text"); - } - else - { - return Theme.getColor("setting_category_text"); - } - } - source: control.checked ? Theme.getIcon("arrow_bottom") : Theme.getIcon("arrow_left") - } - } - } - } - property Component scrollview: Component { ScrollViewStyle @@ -1144,60 +952,4 @@ QtObject label: Item { } } } - - property Component toolbox_action_button: Component - { - ButtonStyle - { - background: Rectangle - { - implicitWidth: UM.Theme.getSize("toolbox_action_button").width - implicitHeight: UM.Theme.getSize("toolbox_action_button").height - color: - { - if (control.installed) - { - return UM.Theme.getColor("action_button_disabled"); - } - else - { - if (control.hovered) - { - return UM.Theme.getColor("primary_hover"); - } - else - { - return UM.Theme.getColor("primary"); - } - } - - } - } - label: Label - { - text: control.text - color: - { - if (control.installed) - { - return UM.Theme.getColor("action_button_disabled_text"); - } - else - { - if (control.hovered) - { - return UM.Theme.getColor("button_text_hover"); - } - else - { - return UM.Theme.getColor("button_text"); - } - } - } - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - font: UM.Theme.getFont("default_bold") - } - } - } } diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 2343cd3f2a..67e0c701c2 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -122,7 +122,6 @@ "text_detail": [174, 174, 174, 128], "text_link": [50, 130, 255, 255], "text_inactive": [174, 174, 174, 255], - "text_hover": [70, 84, 113, 255], "text_pressed": [50, 130, 255, 255], "text_subtext": [0, 0, 0, 255], "text_medium": [128, 128, 128, 255], @@ -228,15 +227,11 @@ "slider_groove": [223, 223, 223, 255], "slider_groove_fill": [10, 8, 80, 255], "slider_handle": [10, 8, 80, 255], - "slider_handle_hover": [77, 182, 226, 255], "slider_handle_active": [50, 130, 255, 255], "slider_text_background": [255, 255, 255, 255], "quality_slider_unavailable": [179, 179, 179, 255], "quality_slider_available": [0, 0, 0, 255], - "quality_slider_handle": [0, 0, 0, 255], - "quality_slider_handle_hover": [127, 127, 127, 255], - "quality_slider_text": [0, 0, 0, 255], "checkbox": [255, 255, 255, 255], "checkbox_hover": [255, 255, 255, 255], @@ -245,15 +240,6 @@ "checkbox_mark": [119, 122, 124, 255], "checkbox_text": [27, 27, 27, 255], - "mode_switch": [255, 255, 255, 255], - "mode_switch_hover": [255, 255, 255, 255], - "mode_switch_border": [127, 127, 127, 255], - "mode_switch_border_hover": [50, 130, 255, 255], - "mode_switch_handle": [31, 36, 39, 255], - "mode_switch_text": [31, 36, 39, 255], - "mode_switch_text_hover": [31, 36, 39, 255], - "mode_switch_text_checked": [50, 130, 255, 255], - "tooltip": [68, 192, 255, 255], "tooltip_text": [255, 255, 255, 255], @@ -384,7 +370,6 @@ "print_setup_item": [0.0, 2.0], "print_setup_extruder_box": [0.0, 6.0], - "configuration_selector_widget": [35.0, 4.5], "configuration_selector_mode_tabs": [0.0, 3.0], "action_panel_widget": [25.0, 0.0], @@ -412,9 +397,6 @@ "extruder_icon": [1.8, 1.8], - "simple_mode_infill_caption": [0.0, 5.0], - "simple_mode_infill_height": [0.0, 8.0], - "section": [0.0, 2.2], "section_icon": [1.6, 1.6], "section_icon_column": [2.8, 0.0], @@ -428,7 +410,6 @@ "setting_text_maxwidth": [40.0, 0.0], "standard_list_lineheight": [1.5, 1.5], - "standard_list_input": [20.0, 25.0], "standard_arrow": [0.8, 0.8], "button": [4, 4], @@ -482,7 +463,6 @@ "modal_window_minimum": [60.0, 45], "license_window_minimum": [45, 45], - "wizard_progress": [10.0, 0.0], "message": [30.0, 5.0], "message_close": [1, 1], From a3b45ff2039c22981643ff501780ee016f9a2bef Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 26 Nov 2018 17:35:23 +0100 Subject: [PATCH 110/146] Simplify the viewOrientation controls --- resources/qml/ViewOrientationButton.qml | 16 +++++++++++++++ resources/qml/ViewOrientationControls.qml | 24 +++++++---------------- 2 files changed, 23 insertions(+), 17 deletions(-) create mode 100644 resources/qml/ViewOrientationButton.qml diff --git a/resources/qml/ViewOrientationButton.qml b/resources/qml/ViewOrientationButton.qml new file mode 100644 index 0000000000..682fd71b4e --- /dev/null +++ b/resources/qml/ViewOrientationButton.qml @@ -0,0 +1,16 @@ +// Copyright (c) 2018 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.2 + +import UM 1.4 as UM + +UM.SimpleButton +{ + width: UM.Theme.getSize("small_button").width + height: UM.Theme.getSize("small_button").height + hoverBackgroundColor: UM.Theme.getColor("small_button_hover") + hoverColor: UM.Theme.getColor("small_button_text_hover") + color: UM.Theme.getColor("small_button_text") + iconMargin: 0.5 * UM.Theme.getSize("wide_lining").width +} \ No newline at end of file diff --git a/resources/qml/ViewOrientationControls.qml b/resources/qml/ViewOrientationControls.qml index acf75b1b48..51ed6e3dcb 100644 --- a/resources/qml/ViewOrientationControls.qml +++ b/resources/qml/ViewOrientationControls.qml @@ -7,7 +7,7 @@ import QtQuick.Controls.Styles 1.1 import UM 1.4 as UM -// View orientation Item +// A row of buttons that control the view direction Row { id: viewOrientationControl @@ -16,43 +16,33 @@ Row height: childrenRect.height width: childrenRect.width - // #1 3d view - Button + ViewOrientationButton { iconSource: UM.Theme.getIcon("view_3d") - style: UM.Theme.styles.small_tool_button - onClicked:UM.Controller.rotateView("3d", 0) + onClicked: UM.Controller.rotateView("3d", 0) } - // #2 Front view - Button + ViewOrientationButton { iconSource: UM.Theme.getIcon("view_front") - style: UM.Theme.styles.small_tool_button onClicked: UM.Controller.rotateView("home", 0) } - // #3 Top view - Button + ViewOrientationButton { iconSource: UM.Theme.getIcon("view_top") - style: UM.Theme.styles.small_tool_button onClicked: UM.Controller.rotateView("y", 90) } - // #4 Left view - Button + ViewOrientationButton { iconSource: UM.Theme.getIcon("view_left") - style: UM.Theme.styles.small_tool_button onClicked: UM.Controller.rotateView("x", 90) } - // #5 Right view - Button + ViewOrientationButton { iconSource: UM.Theme.getIcon("view_right") - style: UM.Theme.styles.small_tool_button onClicked: UM.Controller.rotateView("x", -90) } } From 5ddb3ff90994bd5e5579fc6a8f8ecaeac81a6859 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 26 Nov 2018 17:40:34 +0100 Subject: [PATCH 111/146] Factor out superfluous use of small_tool_button style The style was overly complicated for what it should do --- .../SimulationViewMainComponent.qml | 11 +- resources/themes/cura-light/styles.qml | 112 ------------------ 2 files changed, 8 insertions(+), 115 deletions(-) diff --git a/plugins/SimulationView/SimulationViewMainComponent.qml b/plugins/SimulationView/SimulationViewMainComponent.qml index 4e038a1cf1..16b9aeaae6 100644 --- a/plugins/SimulationView/SimulationViewMainComponent.qml +++ b/plugins/SimulationView/SimulationViewMainComponent.qml @@ -6,7 +6,7 @@ import QtQuick.Controls 1.2 import QtQuick.Layouts 1.1 import QtQuick.Controls.Styles 1.1 -import UM 1.0 as UM +import UM 1.4 as UM import Cura 1.0 as Cura Item @@ -55,11 +55,16 @@ Item } - Button + UM.SimpleButton { id: playButton iconSource: !is_simulation_playing ? "./resources/simulation_resume.svg": "./resources/simulation_pause.svg" - style: UM.Theme.styles.small_tool_button + width: UM.Theme.getSize("small_button").width + height: UM.Theme.getSize("small_button").height + hoverBackgroundColor: UM.Theme.getColor("small_button_hover") + hoverColor: UM.Theme.getColor("small_button_text_hover") + color: UM.Theme.getColor("small_button_text") + iconMargin: 0.5 * UM.Theme.getSize("wide_lining").width visible: !UM.SimulationView.compatibilityMode Connections diff --git a/resources/themes/cura-light/styles.qml b/resources/themes/cura-light/styles.qml index f4aeb95bb3..f00aab44c0 100755 --- a/resources/themes/cura-light/styles.qml +++ b/resources/themes/cura-light/styles.qml @@ -436,118 +436,6 @@ QtObject } } - property Component small_tool_button: Component - { - ButtonStyle - { - background: Item - { - implicitWidth: Theme.getSize("small_button").width; - implicitHeight: Theme.getSize("small_button").height; - - Rectangle - { - id: smallButtonFace; - - anchors.fill: parent; - property bool down: control.pressed || (control.checkable && control.checked); - - color: - { - if(control.customColor !== undefined && control.customColor !== null) - { - return control.customColor - } - else if(control.checkable && control.checked && control.hovered) - { - return Theme.getColor("small_button_active_hover"); - } - else if(control.pressed || (control.checkable && control.checked)) - { - return Theme.getColor("small_button_active"); - } - else if(control.hovered) - { - return Theme.getColor("small_button_hover"); - } - else - { - return Theme.getColor("small_button"); - } - } - Behavior on color { ColorAnimation { duration: 50; } } - - border.width: (control.hasOwnProperty("needBorder") && control.needBorder) ? 2 * screenScaleFactor : 0 - border.color: Theme.getColor("tool_button_border") - - UM.RecolorImage - { - id: smallToolButtonArrow - - width: 5 - height: 5 - sourceSize.width: 5 - sourceSize.height: 5 - visible: control.menu != null; - color: - { - if(control.checkable && control.checked && control.hovered) - { - return Theme.getColor("small_button_text_active_hover"); - } - else if(control.pressed || (control.checkable && control.checked)) - { - return Theme.getColor("small_button_text_active"); - } - else if(control.hovered) - { - return Theme.getColor("small_button_text_hover"); - } - else - { - return Theme.getColor("small_button_text"); - } - } - source: Theme.getIcon("arrow_bottom") - } - } - } - - label: Item - { - UM.RecolorImage - { - anchors.centerIn: parent - opacity: !control.enabled ? 0.2 : 1.0 - source: control.iconSource; - width: Theme.getSize("small_button_icon").width - height: Theme.getSize("small_button_icon").height - color: - { - if(control.checkable && control.checked && control.hovered) - { - return Theme.getColor("small_button_text_active_hover"); - } - else if(control.pressed || (control.checkable && control.checked)) - { - return Theme.getColor("small_button_text_active"); - } - else if(control.hovered) - { - return Theme.getColor("small_button_text_hover"); - } - else - { - return Theme.getColor("small_button_text"); - } - } - - sourceSize: Theme.getSize("small_button_icon") - } - } - } - } - property Component progressbar: Component { ProgressBarStyle From ad3fa9548a2c78f484fef42336670be822335e8f Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Mon, 26 Nov 2018 17:53:38 +0100 Subject: [PATCH 112/146] Add sourceSize to the open file button. Contributes to CURA-5942. --- plugins/PrepareStage/PrepareMenu.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/PrepareStage/PrepareMenu.qml b/plugins/PrepareStage/PrepareMenu.qml index a953c2b5d1..81799206a0 100644 --- a/plugins/PrepareStage/PrepareMenu.qml +++ b/plugins/PrepareStage/PrepareMenu.qml @@ -101,7 +101,8 @@ Item height: UM.Theme.getSize("button_icon").height color: UM.Theme.getColor("toolbar_button_text") - sourceSize: UM.Theme.getSize("button_icon") + sourceSize.width: width + sourceSize.height: height } background: Rectangle From fcc6af68af76db114875479066a63c8660785670 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Mon, 26 Nov 2018 17:55:25 +0100 Subject: [PATCH 113/146] Don't show the current checked version of the firmware if the version number we gather is ZERO. That means that there was a problem getting the right value. Contributes to CURA-5980. --- plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py index 4c60b95824..9efd3e956a 100644 --- a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py +++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py @@ -93,6 +93,11 @@ class FirmwareUpdateCheckerJob(Job): current_version = self.getCurrentVersion() + # This case indicates that was an error checking the version. + # It happens for instance when not connected to internet. + if current_version == self.ZERO_VERSION: + return + # If it is the first time the version is checked, the checked_version is "" setting_key_str = getSettingsKeyForMachine(machine_id) checked_version = Version(Application.getInstance().getPreferences().getValue(setting_key_str)) From 519e2a3399b69e41af048212c45eda6492d7d773 Mon Sep 17 00:00:00 2001 From: pinchies Date: Tue, 27 Nov 2018 15:06:54 +1100 Subject: [PATCH 114/146] uploaded wrong file... oops --- .../extruders/alfawise_u20_extruder_0.def.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 resources/extruders/alfawise_u20_extruder_0.def.json diff --git a/resources/extruders/alfawise_u20_extruder_0.def.json b/resources/extruders/alfawise_u20_extruder_0.def.json new file mode 100644 index 0000000000..2fbe3f1772 --- /dev/null +++ b/resources/extruders/alfawise_u20_extruder_0.def.json @@ -0,0 +1,16 @@ +{ + "id": "alfawise_u20_extruder_0", + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "alfawise_u20", + "position": "0" + }, + + "overrides": { + "extruder_nr": { "default_value": 0 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} \ No newline at end of file From 47ed9e3f5db99114f30c884cea413dc4949544ec Mon Sep 17 00:00:00 2001 From: pinchies Date: Tue, 27 Nov 2018 15:09:36 +1100 Subject: [PATCH 115/146] deleting the old wrong file.... --- resources/extruders/alfawise_u20.def.json | 96 ----------------------- 1 file changed, 96 deletions(-) delete mode 100644 resources/extruders/alfawise_u20.def.json diff --git a/resources/extruders/alfawise_u20.def.json b/resources/extruders/alfawise_u20.def.json deleted file mode 100644 index f6dccce3ee..0000000000 --- a/resources/extruders/alfawise_u20.def.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "name": "Alfawise U20", - "version": 2, - "inherits": "fdmprinter", - "metadata": { - "visible": true, - "author": "Samuel Pinches", - "manufacturer": "Alfawise", - "file_formats": "text/x-gcode", - "preferred_quality_type": "fine", - "machine_extruder_trains": - { - "0": "alfawise_u20_extruder_0" - } - }, - "overrides": { - "machine_name": { - "default_value": "Alfawise U20" - }, - "machine_start_gcode": { - "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 X0 Y0 ;move to the X/Y origin (Home)\nG28 Z0 ;move to the Z origin (Home)\nG1 Z15.0 F6000 ;move Z to position 15.0 mm as fast as possible\nG92 E0 ;zero the extruded length\nG1 X0.0 Y0.0 F1000.0 ;go to edge of print area\nG1 X60.0 E9.0 F1000.0 ;intro line\nG1 X100.0 E21.5 F1000.0 ;intro line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" - }, - "machine_end_gcode": { - "default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nM140 S0 ;turn off bed heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 ;move to the X-axis origin (Home)\nG0 Y280 F600 ;bring the bed to the front for easy print removal\nM84 ;turn off stepper motors\n; -- end of END GCODE --" - }, - "machine_width": { - "default_value": 300 - }, - "machine_height": { - "default_value": 400 - }, - "machine_depth": { - "default_value": 300 - }, - "machine_heated_bed": { - "default_value": true - }, - "machine_center_is_zero": { - "default_value": false - }, - "gantry_height": { - "default_value": 10 - }, - "machine_gcode_flavor": { - "default_value": "RepRap (Marlin/Sprinter)" - }, - "material_diameter": { - "default_value": 1.75 - }, - "material_print_temperature": { - "default_value": 210 - }, - "material_bed_temperature": { - "default_value": 50 - }, - "layer_height": { - "default_value": 0.15 - }, - "layer_height_0": { - "default_value": 0.2 - }, - "wall_thickness": { - "default_value": 1.2 - }, - "speed_print": { - "default_value": 40 - }, - "speed_infill": { - "default_value": 40 - }, - "speed_wall": { - "default_value": 35 - }, - "speed_topbottom": { - "default_value": 35 - }, - "speed_travel": { - "default_value": 120 - }, - "speed_layer_0": { - "default_value": 20 - }, - "support_enable": { - "default_value": true - }, - "retraction_enable": { - "default_value": true - }, - "retraction_amount": { - "default_value": 5 - }, - "retraction_speed": { - "default_value": 45 - } - } -} \ No newline at end of file From 2e81b97623d542c23f7f0243871269e238e8fae9 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 27 Nov 2018 08:45:42 +0100 Subject: [PATCH 116/146] Use global_stack.extruders to find extruders CURA-5978 --- cura/Settings/MachineManager.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index f321ce94a6..f7ad108ad4 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -909,20 +909,14 @@ class MachineManager(QObject): # After CURA-4482 this should not be the case anymore, but we still want to support older project files. global_user_container = self._global_container_stack.userChanges - # Make sure extruder_stacks exists - extruder_stacks = [] #type: List[ExtruderStack] - - if previous_extruder_count == 1: - extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks() - global_user_container = self._global_container_stack.userChanges - for setting_instance in global_user_container.findInstances(): setting_key = setting_instance.definition.key settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder") if settable_per_extruder: limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_extruder")) - extruder_stack = extruder_stacks[max(0, limit_to_extruder)] + extruder_position = str(max(0, limit_to_extruder)) + extruder_stack = self._global_container_stack.extruders[extruder_position] extruder_stack.userChanges.setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value")) global_user_container.removeInstance(setting_key) From 8e7e8354e7196b1e449f43f89c7aa521b9d43931 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 27 Nov 2018 09:41:45 +0100 Subject: [PATCH 117/146] Set colors to the correct ones --- resources/themes/cura-light/theme.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 67e0c701c2..fdc5b2a2de 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -86,8 +86,8 @@ "primary_hover": [48, 182, 231, 255], "primary_text": [255, 255, 255, 255], "border": [127, 127, 127, 255], - "secondary": [245, 245, 245, 255], - "secondary_shadow": [228, 228, 228, 255], + "secondary": [240, 240, 240, 255], + "secondary_shadow": [216, 216, 216, 255], "primary_button": [38, 113, 231, 255], "primary_button_shadow": [27, 95, 202, 255], @@ -95,7 +95,7 @@ "primary_button_text": [255, 255, 255, 255], "secondary_button": [240, 240, 240, 255], - "secondary_button_shadow": [228, 228, 228, 255], + "secondary_button_shadow": [216, 216, 216, 255], "secondary_button_hover": [228, 228, 228, 255], "secondary_button_text": [30, 102, 215, 255], From bd42136712e63abc8043a41ce1004275f4f94d5f Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 27 Nov 2018 09:49:35 +0100 Subject: [PATCH 118/146] Fix GcodeStartEndFormatter to use fallback values CURA-5901 Use values from the global stack (if exist) as fallback values. --- plugins/CuraEngineBackend/StartSliceJob.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index 79b1e5249c..273dc0b6f6 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -66,11 +66,19 @@ class GcodeStartEndFormatter(Formatter): return "{" + key + "}" key = key_fragments[0] - try: - return kwargs[str(extruder_nr)][key] - except KeyError: + + default_value_str = "{" + key + "}" + value = default_value_str + # "-1" is global stack, and if the setting value exists in the global stack, use it as the fallback value. + if key in kwargs["-1"]: + value = kwargs["-1"] + if key in kwargs[str(extruder_nr)]: + value = kwargs[str(extruder_nr)][key] + + if value == default_value_str: Logger.log("w", "Unable to replace '%s' placeholder in start/end g-code", key) - return "{" + key + "}" + + return value ## Job class that builds up the message of scene data to send to CuraEngine. From 39e1cfd6bc080cb193191a1d8472a30b3fb21274 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 27 Nov 2018 09:50:01 +0100 Subject: [PATCH 119/146] Fix indentation. Contributes to CURA-5902. --- resources/definitions/alfawise_u20.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/alfawise_u20.def.json b/resources/definitions/alfawise_u20.def.json index abc206c4d2..87726fec3d 100644 --- a/resources/definitions/alfawise_u20.def.json +++ b/resources/definitions/alfawise_u20.def.json @@ -83,7 +83,7 @@ "support_enable": { "default_value": true }, - "retraction_enable": { + "retraction_enable": { "default_value": true }, "retraction_amount": { From e2f85fcdc4922fada1ec7d259cab11a9c452df87 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 27 Nov 2018 11:25:43 +0100 Subject: [PATCH 120/146] Add extra space to printer button. Contributes to CURA-5942. --- resources/qml/PrinterSelector/MachineSelector.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/qml/PrinterSelector/MachineSelector.qml b/resources/qml/PrinterSelector/MachineSelector.qml index 93e5103aa8..15cd773c90 100644 --- a/resources/qml/PrinterSelector/MachineSelector.qml +++ b/resources/qml/PrinterSelector/MachineSelector.qml @@ -90,11 +90,13 @@ Cura.ExpandableComponent id: scroll width: parent.width clip: true + leftPadding: UM.Theme.getSize("default_lining").width + rightPadding: UM.Theme.getSize("default_lining").width MachineSelectorList { // Can't use parent.width since the parent is the flickable component and not the ScrollView - width: scroll.width + width: scroll.width - scroll.leftPadding - scroll.rightPadding property real maximumHeight: UM.Theme.getSize("machine_selector_widget_content").height - buttonRow.height onHeightChanged: From 75b827d3732f128bd3eacb14d8803d90fa057a4c Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 27 Nov 2018 11:26:20 +0100 Subject: [PATCH 121/146] Modify the hover behavior by removing the mouse area. Contributes to CURA-5942. --- resources/qml/PrinterSelector/MachineSelectorButton.qml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/resources/qml/PrinterSelector/MachineSelectorButton.qml b/resources/qml/PrinterSelector/MachineSelectorButton.qml index 992ea55b1d..369e75cede 100644 --- a/resources/qml/PrinterSelector/MachineSelectorButton.qml +++ b/resources/qml/PrinterSelector/MachineSelectorButton.qml @@ -16,6 +16,7 @@ Button leftPadding: UM.Theme.getSize("thick_margin").width rightPadding: UM.Theme.getSize("thick_margin").width checkable: true + hoverEnabled: true property var outputDevice: null property var printerTypesList: [] @@ -86,14 +87,6 @@ Button Cura.MachineManager.setActiveMachine(model.id) } - MouseArea - { - id: mouseArea - anchors.fill: parent - onPressed: mouse.accepted = false - hoverEnabled: true - } - Connections { target: outputDevice From 44c415ff78fbc085a81e2fc6da08e91c44c0ea8e Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 27 Nov 2018 11:58:32 +0100 Subject: [PATCH 122/146] Add shadow to ExpandableComponent --- resources/qml/ExpandableComponent.qml | 23 ++++++++++++++++++++++- resources/themes/cura-light/theme.json | 2 +- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/resources/qml/ExpandableComponent.qml b/resources/qml/ExpandableComponent.qml index 9b2826daed..b438f0398c 100644 --- a/resources/qml/ExpandableComponent.qml +++ b/resources/qml/ExpandableComponent.qml @@ -4,6 +4,8 @@ import QtQuick.Controls 2.3 import UM 1.2 as UM import Cura 1.0 as Cura +import QtGraphicalEffects 1.0 // For the dropshadow + // The expandable component has 3 major sub components: // * The headerItem; Always visible and should hold some info about what happens if the component is expanded // * The popupItem; The content that needs to be shown if the component is expanded. @@ -58,6 +60,12 @@ Item // On what side should the header corners be shown? 1 is down, 2 is left, 3 is up and 4 is right. property alias headerCornerSide: background.cornerSide + property alias headerShadowColor: shadow.color + + property alias enableHeaderShadow: shadow.visible + + property int shadowOffset: 2 + function togglePopup() { if(popup.visible) @@ -149,13 +157,26 @@ Item onExited: background.color = headerBackgroundColor } } + DropShadow + { + id: shadow + // Don't blur the shadow + radius: 0 + anchors.fill: background + source: background + verticalOffset: base.shadowOffset + visible: true + color: UM.Theme.getColor("action_button_shadow") + // Should always be drawn behind the background. + z: background.z - 1 + } Popup { id: popup // Ensure that the popup is located directly below the headerItem - y: headerItemLoader.height + 2 * background.padding + y: headerItemLoader.height + 2 * background.padding + base.shadowOffset // Make the popup aligned with the rest, using the property popupAlignment to decide whether is right or left. // In case of right alignment, the 3x padding is due to left, right and padding between the button & text. diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 0cfa36fd2e..cc33d82b7d 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -178,7 +178,7 @@ "action_button_disabled": [245, 245, 245, 255], "action_button_disabled_text": [127, 127, 127, 255], "action_button_disabled_border": [245, 245, 245, 255], - "action_button_shadow": [64, 47, 205, 255], + "action_button_shadow": [223, 223, 223, 255], "action_button_disabled_shadow": [228, 228, 228, 255], "scrollbar_background": [255, 255, 255, 255], From e04f14b50c8967d063c90731b6e367ba5c996a9e Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 27 Nov 2018 12:01:05 +0100 Subject: [PATCH 123/146] Also add shadow to openFile button --- plugins/PrepareStage/PrepareMenu.qml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/plugins/PrepareStage/PrepareMenu.qml b/plugins/PrepareStage/PrepareMenu.qml index 81799206a0..31a78ed290 100644 --- a/plugins/PrepareStage/PrepareMenu.qml +++ b/plugins/PrepareStage/PrepareMenu.qml @@ -8,6 +8,7 @@ import QtQuick.Controls 2.3 import UM 1.3 as UM import Cura 1.1 as Cura +import QtGraphicalEffects 1.0 // For the dropshadow Item { @@ -107,12 +108,26 @@ Item background: Rectangle { + id: background height: UM.Theme.getSize("stage_menu").height width: UM.Theme.getSize("stage_menu").height radius: UM.Theme.getSize("default_radius").width color: openFileButton.hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("action_button") } + DropShadow + { + id: shadow + // Don't blur the shadow + radius: 0 + anchors.fill: background + source: background + verticalOffset: 2 + visible: true + color: UM.Theme.getColor("action_button_shadow") + // Should always be drawn behind the background. + z: background.z - 1 + } } } } From 1a6822436ddd22d53553c0d2ddcf549084bf763c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 27 Nov 2018 12:01:43 +0100 Subject: [PATCH 124/146] Add missing HoverEnabled property Some systems, like mine, don't have the hoverEnabled default set to true. --- plugins/PrepareStage/PrepareMenu.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/PrepareStage/PrepareMenu.qml b/plugins/PrepareStage/PrepareMenu.qml index 31a78ed290..4212911011 100644 --- a/plugins/PrepareStage/PrepareMenu.qml +++ b/plugins/PrepareStage/PrepareMenu.qml @@ -93,6 +93,7 @@ Item height: UM.Theme.getSize("stage_menu").height width: UM.Theme.getSize("stage_menu").height onClicked: Cura.Actions.open.trigger() + hoverEnabled: true contentItem: UM.RecolorImage { @@ -115,6 +116,7 @@ Item radius: UM.Theme.getSize("default_radius").width color: openFileButton.hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("action_button") } + DropShadow { id: shadow From fb84b344ecac3683848dbdd71d20e5079bd11818 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 27 Nov 2018 12:58:06 +0100 Subject: [PATCH 125/146] Add gradient to header bar --- resources/qml/Cura.qml | 27 ++++++++++++++++++- resources/qml/MainWindow/MainWindowHeader.qml | 27 ++++++++++++++++++- resources/themes/cura-light/theme.json | 1 + 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 2814bb9eb2..63888a4ee4 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -6,6 +6,7 @@ import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import QtQuick.Layouts 1.1 import QtQuick.Dialogs 1.2 +import QtGraphicalEffects 1.0 import UM 1.3 as UM import Cura 1.1 as Cura @@ -153,7 +154,31 @@ UM.MainWindow } visible: stageMenu.source != "" height: Math.round(UM.Theme.getSize("stage_menu").height / 2) - color: UM.Theme.getColor("main_window_header_background") + + LinearGradient + { + anchors.fill: parent + start: Qt.point(0, 0) + end: Qt.point(parent.width, 0) + gradient: Gradient + { + GradientStop + { + position: 0.0 + color: UM.Theme.getColor("main_window_header_background") + } + GradientStop + { + position: 0.5 + color: UM.Theme.getColor("main_window_header_background_gradient") + } + GradientStop + { + position: 1.0 + color: UM.Theme.getColor("main_window_header_background") + } + } + } } Connections diff --git a/resources/qml/MainWindow/MainWindowHeader.qml b/resources/qml/MainWindow/MainWindowHeader.qml index a24af7ee45..2942a9feb3 100644 --- a/resources/qml/MainWindow/MainWindowHeader.qml +++ b/resources/qml/MainWindow/MainWindowHeader.qml @@ -8,6 +8,7 @@ import QtQuick.Controls.Styles 1.1 import UM 1.4 as UM import Cura 1.0 as Cura +import QtGraphicalEffects 1.0 import "../Account" @@ -17,7 +18,31 @@ Rectangle implicitHeight: UM.Theme.getSize("main_window_header").height implicitWidth: UM.Theme.getSize("main_window_header").width - color: UM.Theme.getColor("main_window_header_background") + + LinearGradient + { + anchors.fill: parent + start: Qt.point(0, 0) + end: Qt.point(parent.width, 0) + gradient: Gradient + { + GradientStop + { + position: 0.0 + color: UM.Theme.getColor("main_window_header_background") + } + GradientStop + { + position: 0.5 + color: UM.Theme.getColor("main_window_header_background_gradient") + } + GradientStop + { + position: 1.0 + color: UM.Theme.getColor("main_window_header_background") + } + } + } Image { diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index cc33d82b7d..157adcf6a5 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -100,6 +100,7 @@ "secondary_button_text": [30, 102, 215, 255], "main_window_header_background": [10, 8, 80, 255], + "main_window_header_background_gradient": [25, 23, 91, 255], "main_window_header_button_text_active": [10, 8, 80, 255], "main_window_header_button_text_inactive": [255, 255, 255, 255], "main_window_header_button_text_hovered": [255, 255, 255, 255], From 0794a2c8c9197c536ffcba090df72edd7d6e7fc6 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 27 Nov 2018 13:03:36 +0100 Subject: [PATCH 126/146] Remove not needed printer and extruder definitions. Contributes to CURA-5902. --- .../bibo2_single_extruder_0.def.json | 98 ------------------- .../bibo2_single_extruder_1.def.json | 98 ------------------- .../bibo2_single_extruder_0_0.def.json | 40 -------- .../bibo2_single_extruder_0_1.def.json | 40 -------- .../bibo2_single_extruder_1_0.def.json | 40 -------- .../bibo2_single_extruder_1_1.def.json | 40 -------- 6 files changed, 356 deletions(-) delete mode 100644 resources/definitions/bibo2_single_extruder_0.def.json delete mode 100644 resources/definitions/bibo2_single_extruder_1.def.json delete mode 100644 resources/extruders/bibo2_single_extruder_0_0.def.json delete mode 100644 resources/extruders/bibo2_single_extruder_0_1.def.json delete mode 100644 resources/extruders/bibo2_single_extruder_1_0.def.json delete mode 100644 resources/extruders/bibo2_single_extruder_1_1.def.json diff --git a/resources/definitions/bibo2_single_extruder_0.def.json b/resources/definitions/bibo2_single_extruder_0.def.json deleted file mode 100644 index 93c7a4e5ae..0000000000 --- a/resources/definitions/bibo2_single_extruder_0.def.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "id": "BIBO2 single E1", - "version": 2, - "name": "BIBO2 single E1", - "inherits": "fdmprinter", - "metadata": { - "visible": true, - "author": "na", - "manufacturer": "BIBO", - "category": "Other", - "file_formats": "text/x-gcode", - "has_materials": true, - "machine_extruder_trains": { - "0": "bibo2_single_extruder_0_0", - "1": "bibo2_single_extruder_0_1" - }, - "first_start_actions": [ - "MachineSettingsAction" - ] - }, - "overrides": { - "machine_name": { - "default_value": "BIBO2 single Extruder 1 (right)" - }, - "machine_width": { - "default_value": 214 - }, - "machine_height": { - "default_value": 160 - }, - "machine_depth": { - "default_value": 186 - }, - "machine_center_is_zero": { - "default_value": true - }, - "machine_heated_bed": { - "default_value": true - }, - "machine_nozzle_size": { - "default_value": 0.4 - }, - "machine_nozzle_heat_up_speed": { - "default_value": 2 - }, - "machine_nozzle_cool_down_speed": { - "default_value": 2 - }, - "machine_head_with_fans_polygon": { - "default_value": [ - [ - -68.18, - 64.63 - ], - [ - -68.18, - -47.38 - ], - [ - 35.18, - 64.63 - ], - [ - 35.18, - -47.38 - ] - ] - }, - "material_diameter": { - "default_value": 1.75 - }, - "gantry_height": { - "default_value": 12 - }, - "machine_use_extruder_offset_to_offset_coords": { - "default_value": true - }, - "machine_gcode_flavor": { - "default_value": "RepRap (Marlin/Sprinter)" - }, - "machine_start_gcode": { - "default_value": ";Startcode BIBO printers\nM109 T1 S170 ;preheat the other extruder, so it will not knock or ruin the print\nG90 ; absolute mode\nG21 ; metric values\nM82 ; Extruder in absolute mode\nM107\nG28\nG1 Z2 F400\nT0\nG90\nG92 E0\nG28\nG1 Y0 F1200 E0\nG92 E0\nG1 X-15.0 Y-92.9 Z0.3 F2400.0\t\t; move to start-line position\nG1 X15.0 Y-92.9 Z0.3 F1000.0 E2\t\t; draw 1st line\nG1 X15.0 Y-92.6 Z0.3 F3000.0\t\t; move to side a little\nG1 X-15.0 Y-92.6 Z0.3 F1000.0 E4\t\t; draw 2nd line\nG1 X-15.0 Y-92.3 Z0.3 F3000.0\t\t; move to side a little\nG1 X15.0 Y-92.3 Z0.3 F1000.0 E6\t\t; draw 3rd line\nG1 X15.0 Y-92 Z0.3 F3000.0\t\t; move to side a little\nG1 X-15.0 Y-92 Z0.3 F1000.0 E8\t\t; draw 4th line\nG1 X-16.0 Y-91.7 Z0.3 F3000.0\t\t; move to side a little\nG1 X16.0 Y-91.7 Z0.3 F1000.0 E10\t\t; draw 5th line\nG1 X16.0 Y-91.4 Z0.3 F3000.0\t\t; move to side a little\nG1 X-16.0 Y-91.4 Z0.3 F1000.0 E12\t\t; draw 5th line\nG1 E11.5 F2400\t\t\t\t; retract filament 0.5mm\nG92 E0\nM117 BIBO Printing..." - }, - "machine_end_gcode": { - "default_value": ";BIBO End GCode\nM107\nG91 ; Relative positioning\nG1 Z1 F100\nM104 T0 S0\nM104 T1 S0\nG1 X-20 Y-20 F3000\nG28 X0 Y0\nG90 ; Absolute positioning\nG92 E0 ; Reset extruder position\nM140 S0 ; Disable heated bed\nM84 ; Turn steppers off\nM117 BIBO Print complete\n " - }, - "machine_extruder_count": { - "default_value": 2 - }, - "prime_tower_position_x": { - "default_value": 50 - }, - "prime_tower_position_y": { - "default_value": 50 - } - } -} - diff --git a/resources/definitions/bibo2_single_extruder_1.def.json b/resources/definitions/bibo2_single_extruder_1.def.json deleted file mode 100644 index 246add09ab..0000000000 --- a/resources/definitions/bibo2_single_extruder_1.def.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "id": "BIBO2 single E2", - "version": 2, - "name": "BIBO2 single E2", - "inherits": "fdmprinter", - "metadata": { - "visible": true, - "author": "na", - "manufacturer": "BIBO", - "category": "Other", - "file_formats": "text/x-gcode", - "has_materials": true, - "machine_extruder_trains": { - "0": "bibo2_single_extruder_1_0", - "1": "bibo2_single_extruder_1_1" - }, - "first_start_actions": [ - "MachineSettingsAction" - ] - }, - "overrides": { - "machine_name": { - "default_value": "BIBO2 single Extruder 2 (left)" - }, - "machine_width": { - "default_value": 214 - }, - "machine_height": { - "default_value": 160 - }, - "machine_depth": { - "default_value": 186 - }, - "machine_center_is_zero": { - "default_value": true - }, - "machine_heated_bed": { - "default_value": true - }, - "machine_nozzle_size": { - "default_value": 0.4 - }, - "machine_nozzle_heat_up_speed": { - "default_value": 2 - }, - "machine_nozzle_cool_down_speed": { - "default_value": 2 - }, - "machine_head_with_fans_polygon": { - "default_value": [ - [ - -68.18, - 64.63 - ], - [ - -68.18, - -47.38 - ], - [ - 35.18, - 64.63 - ], - [ - 35.18, - -47.38 - ] - ] - }, - "material_diameter": { - "default_value": 1.75 - }, - "gantry_height": { - "default_value": 12 - }, - "machine_use_extruder_offset_to_offset_coords": { - "default_value": true - }, - "machine_gcode_flavor": { - "default_value": "RepRap (Marlin/Sprinter)" - }, - "machine_start_gcode": { - "default_value": ";Startcode BIBO printers\nM109 T0 S170 ;preheat the other extruder, so it will not knock or ruin the print\nG90 ; absolute mode\nG21 ; metric values\nM82 ; Extruder in absolute mode\nM107\nG28\nG1 Z2 F400\nT0\nG90\nG92 E0\nG28\nG1 Y0 F1200 E0\nG92 E0\nT1\nG92 E0\nG1 X-15.0 Y-92.9 Z0.3 F2400.0\t\t; move to start-line position\nG1 X15.0 Y-92.9 Z0.3 F1000.0 E2\t\t; draw 1st line\nG1 X15.0 Y-92.6 Z0.3 F3000.0\t\t; move to side a little\nG1 X-15.0 Y-92.6 Z0.3 F1000.0 E4\t\t; draw 2nd line\nG1 X-15.0 Y-92.3 Z0.3 F3000.0\t\t; move to side a little\nG1 X15.0 Y-92.3 Z0.3 F1000.0 E6\t\t; draw 3rd line\nG1 X15.0 Y-92 Z0.3 F3000.0\t\t; move to side a little\nG1 X-15.0 Y-92 Z0.3 F1000.0 E8\t\t; draw 4th line\nG1 X-16.0 Y-91.7 Z0.3 F3000.0\t\t; move to side a little\nG1 X16.0 Y-91.7 Z0.3 F1000.0 E10\t\t; draw 5th line\nG1 X16.0 Y-91.4 Z0.3 F3000.0\t\t; move to side a little\nG1 X-16.0 Y-91.4 Z0.3 F1000.0 E12\t\t; draw 5th line\nG1 E11.5 F2400\t\t\t\t; retract filament 0.5mm\nG92 E0\nM117 BIBO Printing..." - }, - "machine_end_gcode": { - "default_value": ";BIBO End GCode\nM107\nG91 ; Relative positioning\nG1 Z1 F100\nM104 T0 S0\nM104 T1 S0\nG1 X-20 Y-20 F3000\nG28 X0 Y0\nG90 ; Absolute positioning\nG92 E0 ; Reset extruder position\nM140 S0 ; Disable heated bed\nM84 ; Turn steppers off\nM117 BIBO Print complete\n " - }, - "machine_extruder_count": { - "default_value": 2 - }, - "prime_tower_position_x": { - "default_value": 50 - }, - "prime_tower_position_y": { - "default_value": 50 - } - } -} - diff --git a/resources/extruders/bibo2_single_extruder_0_0.def.json b/resources/extruders/bibo2_single_extruder_0_0.def.json deleted file mode 100644 index 7d0b246131..0000000000 --- a/resources/extruders/bibo2_single_extruder_0_0.def.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "id": "BIBO2 E1a", - "version": 2, - "name": "BIBO2 E1", - "inherits": "fdmextruder", - "metadata": { - "machine": "BIBO2 single E1", - "position": "0" - }, - "overrides": { - "extruder_nr": { - "default_value": 0, - "maximum_value": "1" - }, - "machine_nozzle_offset_x": { - "default_value": 0.0 - }, - "machine_nozzle_offset_y": { - "default_value": 0.0 - }, - "machine_extruder_start_pos_abs": { - "default_value": true - }, - "machine_extruder_start_pos_x": { - "value": "prime_tower_position_x" - }, - "machine_extruder_start_pos_y": { - "value": "prime_tower_position_y" - }, - "machine_extruder_end_pos_abs": { - "default_value": true - }, - "machine_extruder_end_pos_x": { - "value": "prime_tower_position_x" - }, - "machine_extruder_end_pos_y": { - "value": "prime_tower_position_y" - } - } -} diff --git a/resources/extruders/bibo2_single_extruder_0_1.def.json b/resources/extruders/bibo2_single_extruder_0_1.def.json deleted file mode 100644 index 76187696fc..0000000000 --- a/resources/extruders/bibo2_single_extruder_0_1.def.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "id": "BIBO2 E1b", - "version": 2, - "name": "E2 not used", - "inherits": "fdmextruder", - "metadata": { - "machine": "BIBO2 single E1", - "position": "1" - }, - "overrides": { - "extruder_nr": { - "default_value": 1, - "maximum_value": "1" - }, - "machine_nozzle_offset_x": { - "default_value": 0.0 - }, - "machine_nozzle_offset_y": { - "default_value": 0.0 - }, - "machine_extruder_start_pos_abs": { - "default_value": true - }, - "machine_extruder_start_pos_x": { - "value": "prime_tower_position_x" - }, - "machine_extruder_start_pos_y": { - "value": "prime_tower_position_y" - }, - "machine_extruder_end_pos_abs": { - "default_value": true - }, - "machine_extruder_end_pos_x": { - "value": "prime_tower_position_x" - }, - "machine_extruder_end_pos_y": { - "value": "prime_tower_position_y" - } - } -} diff --git a/resources/extruders/bibo2_single_extruder_1_0.def.json b/resources/extruders/bibo2_single_extruder_1_0.def.json deleted file mode 100644 index 3cf667de82..0000000000 --- a/resources/extruders/bibo2_single_extruder_1_0.def.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "id": "BIBO2 E2a", - "version": 2, - "name": "E1 not used", - "inherits": "fdmextruder", - "metadata": { - "machine": "BIBO2 single E2", - "position": "0" - }, - "overrides": { - "extruder_nr": { - "default_value": 0, - "maximum_value": "1" - }, - "machine_nozzle_offset_x": { - "default_value": 0 - }, - "machine_nozzle_offset_y": { - "default_value": 0 - }, - "machine_extruder_start_pos_abs": { - "default_value": true - }, - "machine_extruder_start_pos_x": { - "value": "prime_tower_position_x" - }, - "machine_extruder_start_pos_y": { - "value": "prime_tower_position_y" - }, - "machine_extruder_end_pos_abs": { - "default_value": true - }, - "machine_extruder_end_pos_x": { - "value": "prime_tower_position_x" - }, - "machine_extruder_end_pos_y": { - "value": "prime_tower_position_y" - } - } -} diff --git a/resources/extruders/bibo2_single_extruder_1_1.def.json b/resources/extruders/bibo2_single_extruder_1_1.def.json deleted file mode 100644 index e8f3ec7054..0000000000 --- a/resources/extruders/bibo2_single_extruder_1_1.def.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "id": "BIBO2 E2b", - "version": 2, - "name": "BIBO2 E2", - "inherits": "fdmextruder", - "metadata": { - "machine": "BIBO2 single E2", - "position": "1" - }, - "overrides": { - "extruder_nr": { - "default_value": 1, - "maximum_value": "1" - }, - "machine_nozzle_offset_x": { - "default_value": 0 - }, - "machine_nozzle_offset_y": { - "default_value": 0 - }, - "machine_extruder_start_pos_abs": { - "default_value": true - }, - "machine_extruder_start_pos_x": { - "value": "prime_tower_position_x" - }, - "machine_extruder_start_pos_y": { - "value": "prime_tower_position_y" - }, - "machine_extruder_end_pos_abs": { - "default_value": true - }, - "machine_extruder_end_pos_x": { - "value": "prime_tower_position_x" - }, - "machine_extruder_end_pos_y": { - "value": "prime_tower_position_y" - } - } -} From a6a364c337efb4f732247f30e60d14884aabdadc Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 27 Nov 2018 13:12:47 +0100 Subject: [PATCH 127/146] Move material diameter and machine nozzle size to the extruder definition. Contributes to CURA-5902. --- resources/definitions/bibo2_dual.def.json | 6 ------ resources/extruders/bibo2_dual_extruder_0.def.json | 6 ++++++ resources/extruders/bibo2_dual_extruder_1.def.json | 10 ++++++++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/resources/definitions/bibo2_dual.def.json b/resources/definitions/bibo2_dual.def.json index e57fb64ec0..2036290ebd 100644 --- a/resources/definitions/bibo2_dual.def.json +++ b/resources/definitions/bibo2_dual.def.json @@ -37,9 +37,6 @@ "machine_heated_bed": { "default_value": true }, - "machine_nozzle_size": { - "default_value": 0.4 - }, "machine_nozzle_heat_up_speed": { "default_value": 2 }, @@ -66,9 +63,6 @@ ] ] }, - "material_diameter": { - "default_value": 1.75 - }, "gantry_height": { "default_value": 12 }, diff --git a/resources/extruders/bibo2_dual_extruder_0.def.json b/resources/extruders/bibo2_dual_extruder_0.def.json index 7cdc03d504..f83801fa0c 100644 --- a/resources/extruders/bibo2_dual_extruder_0.def.json +++ b/resources/extruders/bibo2_dual_extruder_0.def.json @@ -12,6 +12,12 @@ "default_value": 0, "maximum_value": "1" }, + "material_diameter": { + "default_value": 1.75 + }, + "machine_nozzle_size": { + "default_value": 0.4 + }, "machine_nozzle_offset_x": { "default_value": 0.0 }, diff --git a/resources/extruders/bibo2_dual_extruder_1.def.json b/resources/extruders/bibo2_dual_extruder_1.def.json index daa1504220..5f479ba54b 100644 --- a/resources/extruders/bibo2_dual_extruder_1.def.json +++ b/resources/extruders/bibo2_dual_extruder_1.def.json @@ -12,11 +12,17 @@ "default_value": 1, "maximum_value": "1" }, + "material_diameter": { + "default_value": 1.75 + }, + "machine_nozzle_size": { + "default_value": 0.4 + }, "machine_nozzle_offset_x": { - "default_value": 0 + "default_value": 0.0 }, "machine_nozzle_offset_y": { - "default_value": 0 + "default_value": 0.0 }, "machine_extruder_start_pos_abs": { "default_value": true From fcde6e3a34d7b8f51da08473fedf7aac98b6565f Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 27 Nov 2018 13:28:28 +0100 Subject: [PATCH 128/146] Fix the open file button Now it has the correct size and it doesn't look blurry. Contributes to CURA-5942. --- plugins/PrepareStage/PrepareMenu.qml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/plugins/PrepareStage/PrepareMenu.qml b/plugins/PrepareStage/PrepareMenu.qml index 4212911011..10b4262f01 100644 --- a/plugins/PrepareStage/PrepareMenu.qml +++ b/plugins/PrepareStage/PrepareMenu.qml @@ -95,16 +95,21 @@ Item onClicked: Cura.Actions.open.trigger() hoverEnabled: true - contentItem: UM.RecolorImage + contentItem: Item { - id: buttonIcon - source: UM.Theme.getIcon("load") - width: UM.Theme.getSize("button_icon").width - height: UM.Theme.getSize("button_icon").height - color: UM.Theme.getColor("toolbar_button_text") + anchors.fill: parent + UM.RecolorImage + { + id: buttonIcon + anchors.centerIn: parent + source: UM.Theme.getIcon("load") + width: UM.Theme.getSize("button_icon").width + height: UM.Theme.getSize("button_icon").height + color: UM.Theme.getColor("toolbar_button_text") - sourceSize.width: width - sourceSize.height: height + sourceSize.width: width + sourceSize.height: height + } } background: Rectangle From e863c34f68b3fbe69218d76b3e6b549362468bf1 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 27 Nov 2018 13:33:35 +0100 Subject: [PATCH 129/146] Align the text to the center of the button Contributes to CURA-5942. --- resources/qml/MainWindow/MainWindowHeader.qml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/qml/MainWindow/MainWindowHeader.qml b/resources/qml/MainWindow/MainWindowHeader.qml index 2942a9feb3..34936e9b5a 100644 --- a/resources/qml/MainWindow/MainWindowHeader.qml +++ b/resources/qml/MainWindow/MainWindowHeader.qml @@ -122,6 +122,8 @@ Rectangle text: marketplaceButton.text color: marketplaceButton.hovered ? UM.Theme.getColor("main_window_header_background") : UM.Theme.getColor("primary_text") width: contentWidth + verticalAlignment: Text.AlignVCenter + renderType: Text.NativeRendering } anchors From 3ad1802ab68021d4672c874dfaa7481f79212bb3 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 27 Nov 2018 14:34:29 +0100 Subject: [PATCH 130/146] Prevent a KeyError from messing CURA-5978 --- cura/Settings/MachineManager.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index f7ad108ad4..03afc7edd0 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -915,9 +915,12 @@ class MachineManager(QObject): if settable_per_extruder: limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_extruder")) - extruder_position = str(max(0, limit_to_extruder)) - extruder_stack = self._global_container_stack.extruders[extruder_position] - extruder_stack.userChanges.setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value")) + extruder_position = max(0, limit_to_extruder) + extruder_stack = self.getExtruder(extruder_position) + if extruder_stack: + extruder_stack.userChanges.setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value")) + else: + Logger.log("e", "Unable to find extruder on position %s", extruder_position) global_user_container.removeInstance(setting_key) # Signal that the global stack has changed @@ -926,10 +929,9 @@ class MachineManager(QObject): @pyqtSlot(int, result = QObject) def getExtruder(self, position: int) -> Optional[ExtruderStack]: - extruder = None if self._global_container_stack: - extruder = self._global_container_stack.extruders.get(str(position)) - return extruder + return self._global_container_stack.extruders.get(str(position)) + return None def updateDefaultExtruder(self) -> None: if self._global_container_stack is None: From 854755277c705d3eb7e01e21525b2fe5be570426 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 27 Nov 2018 14:37:01 +0100 Subject: [PATCH 131/146] Fix styling of comments Because sentences should start with capitals --- cura/Settings/MachineManager.py | 43 ++++++++++++++++----------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 03afc7edd0..f22029e083 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -64,7 +64,7 @@ class MachineManager(QObject): self.machine_extruder_material_update_dict = collections.defaultdict(list) #type: Dict[str, List[Callable[[], None]]] - self._instance_container_timer = QTimer() #type: QTimer + self._instance_container_timer = QTimer() # type: QTimer self._instance_container_timer.setInterval(250) self._instance_container_timer.setSingleShot(True) self._instance_container_timer.timeout.connect(self.__emitChangedSignals) @@ -74,7 +74,7 @@ class MachineManager(QObject): self._application.globalContainerStackChanged.connect(self._onGlobalContainerChanged) self._container_registry.containerLoadComplete.connect(self._onContainersChanged) - ## When the global container is changed, active material probably needs to be updated. + # When the global container is changed, active material probably needs to be updated. self.globalContainerChanged.connect(self.activeMaterialChanged) self.globalContainerChanged.connect(self.activeVariantChanged) self.globalContainerChanged.connect(self.activeQualityChanged) @@ -115,15 +115,15 @@ class MachineManager(QObject): self._material_incompatible_message = Message(catalog.i18nc("@info:status", "The selected material is incompatible with the selected machine or configuration."), - title = catalog.i18nc("@info:title", "Incompatible Material")) #type: Message + title = catalog.i18nc("@info:title", "Incompatible Material")) # type: Message - containers = CuraContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId) #type: List[InstanceContainer] + containers = CuraContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId) # type: List[InstanceContainer] if containers: containers[0].nameChanged.connect(self._onMaterialNameChanged) - self._material_manager = self._application.getMaterialManager() #type: MaterialManager - self._variant_manager = self._application.getVariantManager() #type: VariantManager - self._quality_manager = self._application.getQualityManager() #type: QualityManager + self._material_manager = self._application.getMaterialManager() # type: MaterialManager + self._variant_manager = self._application.getVariantManager() # type: VariantManager + self._quality_manager = self._application.getQualityManager() # type: QualityManager # When the materials lookup table gets updated, it can mean that a material has its name changed, which should # be reflected on the GUI. This signal emission makes sure that it happens. @@ -156,7 +156,7 @@ class MachineManager(QObject): blurSettings = pyqtSignal() # Emitted to force fields in the advanced sidebar to un-focus, so they update properly outputDevicesChanged = pyqtSignal() - currentConfigurationChanged = pyqtSignal() # Emitted every time the current configurations of the machine changes + currentConfigurationChanged = pyqtSignal() # Emitted every time the current configurations of the machine changes printerConnectedStatusChanged = pyqtSignal() # Emitted every time the active machine change or the outputdevices change rootMaterialChanged = pyqtSignal() @@ -201,7 +201,7 @@ class MachineManager(QObject): extruder_configuration.hotendID = extruder.variant.getName() if extruder.variant != empty_variant_container else None self._current_printer_configuration.extruderConfigurations.append(extruder_configuration) - # an empty build plate configuration from the network printer is presented as an empty string, so use "" for an + # An empty build plate configuration from the network printer is presented as an empty string, so use "" for an # empty build plate. self._current_printer_configuration.buildplateConfiguration = self._global_container_stack.getProperty("machine_buildplate_type", "value") if self._global_container_stack.variant != empty_variant_container else "" self.currentConfigurationChanged.emit() @@ -247,7 +247,7 @@ class MachineManager(QObject): self.updateNumberExtrudersEnabled() self.globalContainerChanged.emit() - # after switching the global stack we reconnect all the signals and set the variant and material references + # After switching the global stack we reconnect all the signals and set the variant and material references if self._global_container_stack: self._application.getPreferences().setValue("cura/active_machine", self._global_container_stack.getId()) @@ -261,7 +261,7 @@ class MachineManager(QObject): if global_variant.getMetaDataEntry("hardware_type") != "buildplate": self._global_container_stack.setVariant(empty_variant_container) - # set the global material to empty as we now use the extruder stack at all times - CURA-4482 + # Set the global material to empty as we now use the extruder stack at all times - CURA-4482 global_material = self._global_container_stack.material if global_material != empty_material_container: self._global_container_stack.setMaterial(empty_material_container) @@ -419,7 +419,7 @@ class MachineManager(QObject): # Not a very pretty solution, but the extruder manager doesn't really know how many extruders there are machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value") extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks() - count = 1 # we start with the global stack + count = 1 # We start with the global stack for stack in extruder_stacks: md = stack.getMetaData() if "position" in md and int(md["position"]) >= machine_extruder_count: @@ -646,7 +646,7 @@ class MachineManager(QObject): new_value = self._active_container_stack.getProperty(key, "value") extruder_stacks = [stack for stack in ExtruderManager.getInstance().getActiveExtruderStacks()] - # check in which stack the value has to be replaced + # Check in which stack the value has to be replaced for extruder_stack in extruder_stacks: if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value: extruder_stack.userChanges.setProperty(key, "value", new_value) # TODO: nested property access, should be improved @@ -662,7 +662,7 @@ class MachineManager(QObject): for key in self._active_container_stack.userChanges.getAllKeys(): new_value = self._active_container_stack.getProperty(key, "value") - # check if the value has to be replaced + # Check if the value has to be replaced extruder_stack.userChanges.setProperty(key, "value", new_value) @pyqtProperty(str, notify = activeVariantChanged) @@ -731,7 +731,7 @@ class MachineManager(QObject): # If the machine that is being removed is the currently active machine, set another machine as the active machine. activate_new_machine = (self._global_container_stack and self._global_container_stack.getId() == machine_id) - # activate a new machine before removing a machine because this is safer + # Activate a new machine before removing a machine because this is safer if activate_new_machine: machine_stacks = CuraContainerRegistry.getInstance().findContainerStacksMetadata(type = "machine") other_machine_stacks = [s for s in machine_stacks if s["id"] != machine_id] @@ -997,12 +997,12 @@ class MachineManager(QObject): if not enabled and position == ExtruderManager.getInstance().activeExtruderIndex: ExtruderManager.getInstance().setActiveExtruderIndex(int(self._default_extruder_position)) - # ensure that the quality profile is compatible with current combination, or choose a compatible one if available + # Ensure that the quality profile is compatible with current combination, or choose a compatible one if available self._updateQualityWithMaterial() self.extruderChanged.emit() - # update material compatibility color + # Update material compatibility color self.activeQualityGroupChanged.emit() - # update items in SettingExtruder + # Update items in SettingExtruder ExtruderManager.getInstance().extrudersChanged.emit(self._global_container_stack.getId()) # Make sure the front end reflects changes self.forceUpdateAllSettings() @@ -1076,7 +1076,6 @@ class MachineManager(QObject): return result - # # Sets all quality and quality_changes containers to empty_quality and empty_quality_changes containers # for all stacks in the currently active machine. # @@ -1135,7 +1134,7 @@ class MachineManager(QObject): def _setQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None: if self._global_container_stack is None: - return #Can't change that. + return # Can't change that. quality_type = quality_changes_group.quality_type # A custom quality can be created based on "not supported". # In that case, do not set quality containers to empty. @@ -1205,7 +1204,7 @@ class MachineManager(QObject): self.rootMaterialChanged.emit() def activeMaterialsCompatible(self) -> bool: - # check material - variant compatibility + # Check material - variant compatibility if self._global_container_stack is not None: if Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False)): for position, extruder in self._global_container_stack.extruders.items(): @@ -1415,7 +1414,7 @@ class MachineManager(QObject): material_diameter, root_material_id) self.setMaterial(position, material_node) - ## global_stack: if you want to provide your own global_stack instead of the current active one + ## Global_stack: if you want to provide your own global_stack instead of the current active one # if you update an active machine, special measures have to be taken. @pyqtSlot(str, "QVariant") def setMaterial(self, position: str, container_node, global_stack: Optional["GlobalStack"] = None) -> None: From 077b34b4336fee026ee37f43a27e7bb13c9b9b4a Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 27 Nov 2018 14:49:01 +0100 Subject: [PATCH 132/146] Fix size of the extruder icon Now it should look the same in all places. --- resources/qml/ExtruderButton.qml | 14 +++++++++----- resources/qml/ExtruderIcon.qml | 2 -- resources/themes/cura-light/theme.json | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/resources/qml/ExtruderButton.qml b/resources/qml/ExtruderButton.qml index 8a1f0af44a..d7cbdc52bc 100644 --- a/resources/qml/ExtruderButton.qml +++ b/resources/qml/ExtruderButton.qml @@ -19,12 +19,16 @@ Button enabled: UM.Selection.hasSelection && extruder.stack.isEnabled background: Item {} - contentItem: ExtruderIcon + contentItem: Item { - width: UM.Theme.getSize("button_icon").width - materialColor: model.color - extruderEnabled: extruder.stack.isEnabled - property int index: extruder.index + // For some reason if the extruder icon is not enclosed to the item, the size changes to fill the size of the button + ExtruderIcon + { + anchors.centerIn: parent + materialColor: model.color + extruderEnabled: extruder.stack.isEnabled + property int index: extruder.index + } } onClicked: diff --git a/resources/qml/ExtruderIcon.qml b/resources/qml/ExtruderIcon.qml index 8f312adb85..c1a202050b 100644 --- a/resources/qml/ExtruderIcon.qml +++ b/resources/qml/ExtruderIcon.qml @@ -51,8 +51,6 @@ Item anchors.centerIn: parent text: index + 1 font: UM.Theme.getFont("extruder_icon") - width: contentWidth - height: contentHeight visible: extruderEnabled renderType: Text.NativeRendering horizontalAlignment: Text.AlignHCenter diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 157adcf6a5..d2358e36ff 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -402,7 +402,7 @@ "thin_margin": [0.71, 0.71], "narrow_margin": [0.5, 0.5], - "extruder_icon": [1.8, 1.8], + "extruder_icon": [2.33, 2.33], "section": [0.0, 2.2], "section_icon": [1.6, 1.6], From 82ea562dc389b6e3f8872b24c84e4a70f382ffe3 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 27 Nov 2018 14:54:49 +0100 Subject: [PATCH 133/146] Add missing hoverEnabled property --- resources/qml/Settings/SettingCategory.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/qml/Settings/SettingCategory.qml b/resources/qml/Settings/SettingCategory.qml index 7995619af0..00d23580b2 100644 --- a/resources/qml/Settings/SettingCategory.qml +++ b/resources/qml/Settings/SettingCategory.qml @@ -14,6 +14,7 @@ Button anchors.right: parent.right anchors.leftMargin: UM.Theme.getSize("thick_margin").width anchors.rightMargin: UM.Theme.getSize("thick_margin").width + hoverEnabled: true background: Rectangle { id: backgroundRectangle From 92d07170964a8a4906ca2742eeb3675e4619487b Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 27 Nov 2018 15:00:50 +0100 Subject: [PATCH 134/146] Hide the toolbar and don't show time and material information when loading a gcode Contributes to CURA-5979. --- resources/qml/ActionPanel/OutputProcessWidget.qml | 8 +++++++- resources/qml/Cura.qml | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/resources/qml/ActionPanel/OutputProcessWidget.qml b/resources/qml/ActionPanel/OutputProcessWidget.qml index ddbe709a84..45cb1fdb41 100644 --- a/resources/qml/ActionPanel/OutputProcessWidget.qml +++ b/resources/qml/ActionPanel/OutputProcessWidget.qml @@ -18,6 +18,7 @@ Column id: widget spacing: UM.Theme.getSize("thin_margin").height + property bool preSlicedData: PrintInformation.preSliced UM.I18nCatalog { @@ -48,7 +49,7 @@ Column id: estimatedTime width: parent.width - text: PrintInformation.currentPrintTime.getDisplayString(UM.DurationFormat.Long) + text: preSlicedData ? catalog.i18nc("@label", "No time estimation available") : PrintInformation.currentPrintTime.getDisplayString(UM.DurationFormat.Long) source: UM.Theme.getIcon("clock") font: UM.Theme.getFont("small") } @@ -63,6 +64,10 @@ Column text: { + if (preSlicedData) + { + return catalog.i18nc("@label", "No cost estimation available") + } var totalLengths = 0 var totalWeights = 0 if (printMaterialLengths) @@ -86,6 +91,7 @@ Column PrintInformationWidget { id: printInformationPanel + visible: !preSlicedData anchors { diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 63888a4ee4..36f5758fa3 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -212,7 +212,7 @@ UM.MainWindow verticalCenter: parent.verticalCenter left: parent.left } - visible: CuraApplication.platformActivity + visible: CuraApplication.platformActivity && !PrintInformation.preSliced } ObjectsList From 4fab546425450142f17a3aaf05511fe1de98b019 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 27 Nov 2018 15:00:57 +0100 Subject: [PATCH 135/146] Fix codestyle issues Boyscouting --- resources/qml/Settings/SettingCategory.qml | 199 +++++++++++---------- 1 file changed, 109 insertions(+), 90 deletions(-) diff --git a/resources/qml/Settings/SettingCategory.qml b/resources/qml/Settings/SettingCategory.qml index 00d23580b2..aafe36c546 100644 --- a/resources/qml/Settings/SettingCategory.qml +++ b/resources/qml/Settings/SettingCategory.qml @@ -15,23 +15,31 @@ Button anchors.leftMargin: UM.Theme.getSize("thick_margin").width anchors.rightMargin: UM.Theme.getSize("thick_margin").width hoverEnabled: true + background: Rectangle { id: backgroundRectangle implicitHeight: UM.Theme.getSize("section").height - color: { - if (base.color) { - return base.color; - } else if (!base.enabled) { - return UM.Theme.getColor("setting_category_disabled"); - } else if (base.hovered && base.checkable && base.checked) { - return UM.Theme.getColor("setting_category_active_hover"); - } else if (base.pressed || (base.checkable && base.checked)) { - return UM.Theme.getColor("setting_category_active"); - } else if (base.hovered) { - return UM.Theme.getColor("setting_category_hover"); - } else { - return UM.Theme.getColor("setting_category"); + color: + { + if (base.color) + { + return base.color + } else if (!base.enabled) + { + return UM.Theme.getColor("setting_category_disabled") + } else if (base.hovered && base.checkable && base.checked) + { + return UM.Theme.getColor("setting_category_active_hover") + } else if (base.pressed || (base.checkable && base.checked)) + { + return UM.Theme.getColor("setting_category_active") + } else if (base.hovered) + { + return UM.Theme.getColor("setting_category_hover") + } else + { + return UM.Theme.getColor("setting_category") } } Behavior on color { ColorAnimation { duration: 50; } } @@ -41,17 +49,23 @@ Button height: UM.Theme.getSize("default_lining").height width: parent.width anchors.bottom: parent.bottom - color: { - if (!base.enabled) { - return UM.Theme.getColor("setting_category_disabled_border"); - } else if ((base.hovered || base.activeFocus) && base.checkable && base.checked) { - return UM.Theme.getColor("setting_category_active_hover_border"); - } else if (base.pressed || (base.checkable && base.checked)) { - return UM.Theme.getColor("setting_category_active_border"); - } else if (base.hovered || base.activeFocus) { - return UM.Theme.getColor("setting_category_hover_border"); - } else { - return UM.Theme.getColor("setting_category_border"); + color: + { + if (!base.enabled) + { + return UM.Theme.getColor("setting_category_disabled_border") + } else if ((base.hovered || base.activeFocus) && base.checkable && base.checked) + { + return UM.Theme.getColor("setting_category_active_hover_border") + } else if (base.pressed || (base.checkable && base.checked)) + { + return UM.Theme.getColor("setting_category_active_border") + } else if (base.hovered || base.activeFocus) + { + return UM.Theme.getColor("setting_category_hover_border") + } else + { + return UM.Theme.getColor("setting_category_border") } } } @@ -66,18 +80,19 @@ Button property var focusItem: base - contentItem: Item { + contentItem: Item + { anchors.fill: parent - anchors.left: parent.left - Label { + Label + { id: settingNameLabel anchors { left: parent.left leftMargin: 2 * UM.Theme.getSize("default_margin").width + UM.Theme.getSize("section_icon").width - right: parent.right; - verticalCenter: parent.verticalCenter; + right: parent.right + verticalCenter: parent.verticalCenter } text: definition.label textFormat: Text.PlainText @@ -85,21 +100,27 @@ Button font: UM.Theme.getFont("setting_category") color: { - if (!base.enabled) { - return UM.Theme.getColor("setting_category_disabled_text"); - } else if ((base.hovered || base.activeFocus) && base.checkable && base.checked) { - return UM.Theme.getColor("setting_category_active_hover_text"); - } else if (base.pressed || (base.checkable && base.checked)) { - return UM.Theme.getColor("setting_category_active_text"); - } else if (base.hovered || base.activeFocus) { - return UM.Theme.getColor("setting_category_hover_text"); - } else { - return UM.Theme.getColor("setting_category_text"); + if (!base.enabled) + { + return UM.Theme.getColor("setting_category_disabled_text") + } else if ((base.hovered || base.activeFocus) && base.checkable && base.checked) + { + return UM.Theme.getColor("setting_category_active_hover_text") + } else if (base.pressed || (base.checkable && base.checked)) + { + return UM.Theme.getColor("setting_category_active_text") + } else if (base.hovered || base.activeFocus) + { + return UM.Theme.getColor("setting_category_hover_text") + } else + { + return UM.Theme.getColor("setting_category_text") } } fontSizeMode: Text.HorizontalFit minimumPointSize: 8 } + UM.RecolorImage { id: category_arrow @@ -112,16 +133,21 @@ Button sourceSize.height: width color: { - if (!base.enabled) { - return UM.Theme.getColor("setting_category_disabled_text"); - } else if ((base.hovered || base.activeFocus) && base.checkable && base.checked) { - return UM.Theme.getColor("setting_category_active_hover_text"); - } else if (base.pressed || (base.checkable && base.checked)) { - return UM.Theme.getColor("setting_category_active_text"); - } else if (base.hovered || base.activeFocus) { - return UM.Theme.getColor("setting_category_hover_text"); - } else { - return UM.Theme.getColor("setting_category_text"); + if (!base.enabled) + { + return UM.Theme.getColor("setting_category_disabled_text") + } else if ((base.hovered || base.activeFocus) && base.checkable && base.checked) + { + return UM.Theme.getColor("setting_category_active_hover_text") + } else if (base.pressed || (base.checkable && base.checked)) + { + return UM.Theme.getColor("setting_category_active_text") + } else if (base.hovered || base.activeFocus) + { + return UM.Theme.getColor("setting_category_hover_text") + } else + { + return UM.Theme.getColor("setting_category_text") } } source: base.checked ? UM.Theme.getIcon("arrow_bottom") : UM.Theme.getIcon("arrow_left") @@ -136,21 +162,26 @@ Button anchors.leftMargin: UM.Theme.getSize("default_margin").width color: { - if (!base.enabled) { - return UM.Theme.getColor("setting_category_disabled_text"); - } else if((base.hovered || base.activeFocus) && base.checkable && base.checked) { - return UM.Theme.getColor("setting_category_active_hover_text"); - } else if(base.pressed || (base.checkable && base.checked)) { - return UM.Theme.getColor("setting_category_active_text"); - } else if(base.hovered || base.activeFocus) { - return UM.Theme.getColor("setting_category_hover_text"); - } else { - return UM.Theme.getColor("setting_category_text"); + if (!base.enabled) + { + return UM.Theme.getColor("setting_category_disabled_text") + } else if((base.hovered || base.activeFocus) && base.checkable && base.checked) + { + return UM.Theme.getColor("setting_category_active_hover_text") + } else if(base.pressed || (base.checkable && base.checked)) + { + return UM.Theme.getColor("setting_category_active_text") + } else if(base.hovered || base.activeFocus) + { + return UM.Theme.getColor("setting_category_hover_text") + } else + { + return UM.Theme.getColor("setting_category_text") } } source: UM.Theme.getIcon(definition.icon) - width: UM.Theme.getSize("section_icon").width; - height: UM.Theme.getSize("section_icon").height; + width: UM.Theme.getSize("section_icon").width + height: UM.Theme.getSize("section_icon").height sourceSize.width: width + 15 * screenScaleFactor sourceSize.height: width + 15 * screenScaleFactor } @@ -160,31 +191,26 @@ Button onClicked: { - if (definition.expanded) { - settingDefinitionsModel.collapse(definition.key); + if (definition.expanded) + { + settingDefinitionsModel.collapse(definition.key) } else { - settingDefinitionsModel.expandRecursive(definition.key); + settingDefinitionsModel.expandRecursive(definition.key) } //Set focus so that tab navigation continues from this point on. //NB: This must be set AFTER collapsing/expanding the category so that the scroll position is correct. - forceActiveFocus(); + forceActiveFocus() } onActiveFocusChanged: { if(activeFocus) { - base.focusReceived(); + base.focusReceived() } } - Keys.onTabPressed: - { - base.setActiveFocusToNextSetting(true) - } - Keys.onBacktabPressed: - { - base.setActiveFocusToNextSetting(false) - } + Keys.onTabPressed: base.setActiveFocusToNextSetting(true) + Keys.onBacktabPressed: base.setActiveFocusToNextSetting(false) UM.SimpleButton { @@ -194,9 +220,10 @@ Button height: Math.round(base.height * 0.6) width: Math.round(base.height * 0.6) - anchors { + anchors + { right: inheritButton.visible ? inheritButton.left : parent.right - // use 1.9 as the factor because there is a 0.1 difference between the settings and inheritance warning icons + // Use 1.9 as the factor because there is a 0.1 difference between the settings and inheritance warning icons rightMargin: inheritButton.visible ? Math.round(UM.Theme.getSize("default_margin").width / 2) : category_arrow.width + Math.round(UM.Theme.getSize("default_margin").width * 1.9) verticalCenter: parent.verticalCenter } @@ -205,9 +232,7 @@ Button hoverColor: UM.Theme.getColor("setting_control_button_hover") iconSource: UM.Theme.getIcon("settings") - onClicked: { - Cura.Actions.configureSettingVisibility.trigger(definition) - } + onClicked: Cura.Actions.configureSettingVisibility.trigger(definition) } UM.SimpleButton @@ -240,24 +265,18 @@ Button onClicked: { - settingDefinitionsModel.expandRecursive(definition.key); - base.checked = true; - base.showAllHiddenInheritedSettings(definition.key); + settingDefinitionsModel.expandRecursive(definition.key) + base.checked = true + base.showAllHiddenInheritedSettings(definition.key) } color: UM.Theme.getColor("setting_control_button") hoverColor: UM.Theme.getColor("setting_control_button_hover") iconSource: UM.Theme.getIcon("notice") - onEntered: - { - base.showTooltip(catalog.i18nc("@label","Some hidden settings use values different from their normal calculated value.\n\nClick to make these settings visible.")) - } + onEntered: base.showTooltip(catalog.i18nc("@label","Some hidden settings use values different from their normal calculated value.\n\nClick to make these settings visible.")) - onExited: - { - base.hideTooltip(); - } + onExited: base.hideTooltip() UM.I18nCatalog { id: catalog; name: "cura" } } From dad2031e4b5b8750a6721e6fdb3a24620eaf3cc7 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 27 Nov 2018 16:06:37 +0100 Subject: [PATCH 136/146] Switch to the preview stage before to switch to simulation view ... since now the list of views are in a different stage. Contributes to CURA-5979. --- cura/CuraApplication.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 6e52a97518..b43e4d5e0e 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1663,7 +1663,9 @@ class CuraApplication(QtApplication): is_non_sliceable = "." + file_extension in self._non_sliceable_extensions if is_non_sliceable: - self.callLater(lambda: self.getController().setActiveView("SimulationView")) + # Need to switch first to the preview stage and then to layer view + self.callLater(lambda: (self.getController().setActiveStage("PreviewStage"), + self.getController().setActiveView("SimulationView"))) block_slicing_decorator = BlockSlicingDecorator() node.addDecorator(block_slicing_decorator) From 309061ce3113df6dd7f7c9e9762526b1ce0973c5 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 27 Nov 2018 17:16:52 +0100 Subject: [PATCH 137/146] Add a new ToolbarButton Now also the Extruder button is a toolbar button since it will show in the toolbar. --- resources/qml/ExtruderButton.qml | 18 +++---- resources/qml/Toolbar.qml | 25 ++++++--- resources/qml/ToolbarButton.qml | 92 ++++++++++++++++++++++++++++++++ resources/qml/qmldir | 3 +- 4 files changed, 117 insertions(+), 21 deletions(-) create mode 100644 resources/qml/ToolbarButton.qml diff --git a/resources/qml/ExtruderButton.qml b/resources/qml/ExtruderButton.qml index d7cbdc52bc..feb399d528 100644 --- a/resources/qml/ExtruderButton.qml +++ b/resources/qml/ExtruderButton.qml @@ -7,7 +7,7 @@ import QtQuick.Controls 2.0 import UM 1.2 as UM import Cura 1.0 as Cura -Button +Cura.ToolbarButton { id: base @@ -18,22 +18,16 @@ Button checked: Cura.ExtruderManager.selectedObjectExtruders.indexOf(extruder.id) != -1 enabled: UM.Selection.hasSelection && extruder.stack.isEnabled - background: Item {} - contentItem: Item + toolItem: ExtruderIcon { - // For some reason if the extruder icon is not enclosed to the item, the size changes to fill the size of the button - ExtruderIcon - { - anchors.centerIn: parent - materialColor: model.color - extruderEnabled: extruder.stack.isEnabled - property int index: extruder.index - } + materialColor: extruder.color + extruderEnabled: extruder.stack.isEnabled + property int index: extruder.index } onClicked: { forceActiveFocus() //First grab focus, so all the text fields are updated - CuraActions.setExtruderForSelection(extruder.id); + CuraActions.setExtruderForSelection(extruder.id) } } diff --git a/resources/qml/Toolbar.qml b/resources/qml/Toolbar.qml index 0240aaab26..3a4e7704c0 100644 --- a/resources/qml/Toolbar.qml +++ b/resources/qml/Toolbar.qml @@ -55,17 +55,25 @@ Item model: UM.ToolModel { id: toolsModel } width: childrenRect.width height: childrenRect.height - Button + + delegate: ToolbarButton { text: model.name + (model.shortcut ? (" (" + model.shortcut + ")") : "") - iconSource: (UM.Theme.getIcon(model.icon) != "") ? UM.Theme.getIcon(model.icon) : "file:///" + model.location + "/" + model.icon checkable: true checked: model.active enabled: model.enabled && UM.Selection.hasSelection && UM.Controller.toolsEnabled - style: UM.Theme.styles.toolbar_button - property bool isFirstElement: toolsModel.getItem(0).id == model.id - property bool isLastElement: toolsModel.getItem(toolsModel.rowCount() - 1).id == model.id + topElement: toolsModel.getItem(0).id == model.id + bottomElement: toolsModel.getItem(toolsModel.rowCount() - 1).id == model.id + + toolItem: UM.RecolorImage + { + opacity: parent.enabled ? 1.0 : 0.2 + source: (UM.Theme.getIcon(model.icon) != "") ? UM.Theme.getIcon(model.icon) : "file:///" + model.location + "/" + model.icon + color: UM.Theme.getColor("toolbar_button_text") + + sourceSize: UM.Theme.getSize("button_icon") + } onCheckedChanged: { @@ -128,11 +136,12 @@ Item height: childrenRect.height property var _model: Cura.ExtrudersModel { id: extrudersModel } model: _model.items.length > 1 ? _model : 0 - ExtruderButton + + delegate: ExtruderButton { extruder: model - height: UM.Theme.getSize("button").width - width: UM.Theme.getSize("button").width + topElement: extrudersModel.getItem(0).id == model.id + bottomElement: extrudersModel.getItem(extrudersModel.rowCount() - 1).id == model.id } } } diff --git a/resources/qml/ToolbarButton.qml b/resources/qml/ToolbarButton.qml new file mode 100644 index 0000000000..7241b489b6 --- /dev/null +++ b/resources/qml/ToolbarButton.qml @@ -0,0 +1,92 @@ +// Copyright (c) 2018 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.7 +import QtQuick.Controls 2.3 + +import UM 1.2 as UM +import Cura 1.0 as Cura + +Button +{ + id: base + + property alias toolItem: contentItemLoader.sourceComponent + property bool topElement: false + property bool bottomElement: false + + hoverEnabled: true + + background: Rectangle + { + implicitWidth: UM.Theme.getSize("button").width + implicitHeight: UM.Theme.getSize("button").height + color: + { + if (base.checked && base.hovered) + { + return UM.Theme.getColor("toolbar_button_active_hover") + } + else if (base.checked) + { + return "red" //UM.Theme.getColor("toolbar_button_active") + } + else if(base.hovered) + { + return UM.Theme.getColor("toolbar_button_hover") + } + return UM.Theme.getColor("toolbar_background") + } + radius: UM.Theme.getSize("default_radius").width + + Rectangle + { + id: topSquare + anchors + { + left: parent.left + right: parent.right + top: parent.top + } + height: parent.radius + color: base.topElement ? "transparent" : parent.color + } + + Rectangle + { + id: bottomSquare + anchors + { + left: parent.left + right: parent.right + bottom: parent.bottom + } + height: parent.radius + color: base.bottomElement ? "transparent" : parent.color + } + + Rectangle + { + id: leftSquare + anchors + { + left: parent.left + top: parent.top + bottom: parent.bottom + } + width: parent.radius + color: parent.color + } + } + + contentItem: Item + { + Loader + { + id: contentItemLoader + anchors.centerIn: parent + width: UM.Theme.getSize("button_icon").width + height: UM.Theme.getSize("button_icon").height + } + } +} diff --git a/resources/qml/qmldir b/resources/qml/qmldir index 43ae4411af..2475f398f8 100644 --- a/resources/qml/qmldir +++ b/resources/qml/qmldir @@ -12,4 +12,5 @@ IconLabel 1.0 IconLabel.qml OutputDevicesActionButton 1.0 OutputDevicesActionButton.qml ExpandableComponent 1.0 ExpandableComponent.qml PrinterTypeLabel 1.0 PrinterTypeLabel.qml -ViewsSelector 1.0 ViewsSelector.qml \ No newline at end of file +ViewsSelector 1.0 ViewsSelector.qml +ToolbarButton 1.0 ToolbarButton.qml \ No newline at end of file From 14b460a626bfbb58b0bc8eff6621f46e8d400ecb Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Tue, 27 Nov 2018 17:25:10 +0100 Subject: [PATCH 138/146] Remove unused styles --- resources/qml/Toolbar.qml | 5 +- resources/qml/ToolbarButton.qml | 3 +- resources/themes/cura-light/styles.qml | 117 ------------------------- 3 files changed, 3 insertions(+), 122 deletions(-) diff --git a/resources/qml/Toolbar.qml b/resources/qml/Toolbar.qml index 3a4e7704c0..0207c8ec49 100644 --- a/resources/qml/Toolbar.qml +++ b/resources/qml/Toolbar.qml @@ -2,9 +2,7 @@ // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.2 -import QtQuick.Controls 1.1 -import QtQuick.Controls.Styles 1.1 -import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.3 import UM 1.2 as UM import Cura 1.0 as Cura @@ -68,7 +66,6 @@ Item toolItem: UM.RecolorImage { - opacity: parent.enabled ? 1.0 : 0.2 source: (UM.Theme.getIcon(model.icon) != "") ? UM.Theme.getIcon(model.icon) : "file:///" + model.location + "/" + model.icon color: UM.Theme.getColor("toolbar_button_text") diff --git a/resources/qml/ToolbarButton.qml b/resources/qml/ToolbarButton.qml index 7241b489b6..b5e5aab475 100644 --- a/resources/qml/ToolbarButton.qml +++ b/resources/qml/ToolbarButton.qml @@ -29,7 +29,7 @@ Button } else if (base.checked) { - return "red" //UM.Theme.getColor("toolbar_button_active") + return UM.Theme.getColor("toolbar_button_active") } else if(base.hovered) { @@ -81,6 +81,7 @@ Button contentItem: Item { + opacity: parent.enabled ? 1.0 : 0.2 Loader { id: contentItemLoader diff --git a/resources/themes/cura-light/styles.qml b/resources/themes/cura-light/styles.qml index f00aab44c0..e040d91df9 100755 --- a/resources/themes/cura-light/styles.qml +++ b/resources/themes/cura-light/styles.qml @@ -171,123 +171,6 @@ QtObject } } - property Component toolbar_button: Component - { - ButtonStyle - { - background: Rectangle - { - implicitWidth: Theme.getSize("button").width - implicitHeight: Theme.getSize("button").height - color: - { - if (control.checked && control.hovered) - { - return Theme.getColor("toolbar_button_active_hover") - } - else if (control.checked) - { - return Theme.getColor("toolbar_button_active") - } - else if(control.hovered) - { - return Theme.getColor("toolbar_button_hover") - } - return Theme.getColor("toolbar_background") - } - radius: UM.Theme.getSize("default_radius").width - - Rectangle - { - id: topSquare - anchors - { - left: parent.left - right: parent.right - top: parent.top - } - height: parent.radius - color: control.isFirstElement ? "transparent" : parent.color - } - - Rectangle - { - id: bottomSquare - anchors - { - left: parent.left - right: parent.right - bottom: parent.bottom - } - height: parent.radius - color: control.isLastElement ? "transparent" : parent.color - } - - Rectangle - { - id: leftSquare - anchors - { - left: parent.left - top: parent.top - bottom: parent.bottom - } - width: parent.radius - color: parent.color - } - - // This is the tooltip - UM.PointingRectangle - { - id: button_tooltip - - anchors.left: parent.right - anchors.leftMargin: Theme.getSize("button_tooltip_arrow").width * 2 - anchors.verticalCenter: parent.verticalCenter - - target: Qt.point(parent.x, y + Math.round(height/2)) - arrowSize: Theme.getSize("button_tooltip_arrow").width - color: Theme.getColor("button_tooltip") - opacity: control.hovered ? 1.0 : 0.0; - visible: control.text != "" - - width: control.hovered ? button_tip.width + Theme.getSize("button_tooltip").width : 0 - height: Theme.getSize("button_tooltip").height - - Behavior on width { NumberAnimation { duration: 100; } } - Behavior on opacity { NumberAnimation { duration: 100; } } - - Label - { - id: button_tip - - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter; - - text: control.text; - font: Theme.getFont("button_tooltip"); - color: Theme.getColor("tooltip_text"); - } - } - } - - label: Item - { - UM.RecolorImage - { - anchors.centerIn: parent; - opacity: !control.enabled ? 0.2 : 1.0 - source: control.iconSource; - width: Theme.getSize("button_icon").width; - height: Theme.getSize("button_icon").height; - color: Theme.getColor("toolbar_button_text"); - - sourceSize: Theme.getSize("button_icon") - } - } - } - } - property Component tool_button: Component { ButtonStyle From d8c430abf61c2537720270603796b368f00642d2 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 27 Nov 2018 17:54:53 +0100 Subject: [PATCH 139/146] Fix typing --- cura/CuraActions.py | 14 ++++++++------ cura/PrintInformation.py | 7 +++---- .../PostProcessingPlugin/PostProcessingPlugin.py | 4 ++-- plugins/USBPrinting/AutoDetectBaudJob.py | 11 ++++++----- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/cura/CuraActions.py b/cura/CuraActions.py index 93a18318df..49f7e740a9 100644 --- a/cura/CuraActions.py +++ b/cura/CuraActions.py @@ -3,7 +3,7 @@ from PyQt5.QtCore import QObject, QUrl from PyQt5.QtGui import QDesktopServices -from typing import List, TYPE_CHECKING +from typing import List, TYPE_CHECKING, cast from UM.Event import CallFunctionEvent from UM.FlameProfiler import pyqtSlot @@ -61,8 +61,10 @@ class CuraActions(QObject): operation = GroupedOperation() for node in Selection.getAllSelectedObjects(): current_node = node - while current_node.getParent() and current_node.getParent().callDecoration("isGroup"): - current_node = current_node.getParent() + parent_node = current_node.getParent() + while parent_node and parent_node.callDecoration("isGroup"): + current_node = parent_node + parent_node = current_node.getParent() # This was formerly done with SetTransformOperation but because of # unpredictable matrix deconstruction it was possible that mirrors @@ -150,13 +152,13 @@ class CuraActions(QObject): root = cura.CuraApplication.CuraApplication.getInstance().getController().getScene().getRoot() - nodes_to_change = [] + nodes_to_change = [] # type: List[SceneNode] for node in Selection.getAllSelectedObjects(): parent_node = node # Find the parent node to change instead while parent_node.getParent() != root: - parent_node = parent_node.getParent() + parent_node = cast(SceneNode, parent_node.getParent()) - for single_node in BreadthFirstIterator(parent_node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. + for single_node in BreadthFirstIterator(parent_node): # type: ignore #Ignore type error because iter() should get called automatically by Python syntax. nodes_to_change.append(single_node) if not nodes_to_change: diff --git a/cura/PrintInformation.py b/cura/PrintInformation.py index e11f70a54c..22c3eb1734 100644 --- a/cura/PrintInformation.py +++ b/cura/PrintInformation.py @@ -14,8 +14,7 @@ from UM.Logger import Logger from UM.Qt.Duration import Duration from UM.Scene.SceneNode import SceneNode from UM.i18n import i18nCatalog -from UM.MimeTypeDatabase import MimeTypeDatabase - +from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError from typing import TYPE_CHECKING @@ -361,7 +360,7 @@ class PrintInformation(QObject): try: mime_type = MimeTypeDatabase.getMimeTypeForFile(name) data = mime_type.stripExtension(name) - except: + except MimeTypeNotFoundError: Logger.log("w", "Unsupported Mime Type Database file extension %s", name) if data is not None and check_name is not None: @@ -416,7 +415,7 @@ class PrintInformation(QObject): return ''.join(char for char in unicodedata.normalize('NFD', to_strip) if unicodedata.category(char) != 'Mn') @pyqtSlot(result = "QVariantMap") - def getFeaturePrintTimes(self): + def getFeaturePrintTimes(self) -> Dict[str, Duration]: result = {} if self._active_build_plate not in self._print_times_per_feature: self._initPrintTimesPerFeature(self._active_build_plate) diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.py b/plugins/PostProcessingPlugin/PostProcessingPlugin.py index 11ee610bec..78f9cc0516 100644 --- a/plugins/PostProcessingPlugin/PostProcessingPlugin.py +++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.py @@ -55,14 +55,14 @@ class PostProcessingPlugin(QObject, Extension): def selectedScriptDefinitionId(self) -> Optional[str]: try: return self._script_list[self._selected_script_index].getDefinitionId() - except: + except IndexError: return "" @pyqtProperty(str, notify=selectedIndexChanged) def selectedScriptStackId(self) -> Optional[str]: try: return self._script_list[self._selected_script_index].getStackId() - except: + except IndexError: return "" ## Execute all post-processing scripts on the gcode. diff --git a/plugins/USBPrinting/AutoDetectBaudJob.py b/plugins/USBPrinting/AutoDetectBaudJob.py index 8b37c4b29d..6f1af6727a 100644 --- a/plugins/USBPrinting/AutoDetectBaudJob.py +++ b/plugins/USBPrinting/AutoDetectBaudJob.py @@ -3,6 +3,7 @@ from UM.Job import Job from UM.Logger import Logger +from plugins.USBPrinting.avr_isp import ispBase from .avr_isp.stk500v2 import Stk500v2 @@ -14,12 +15,12 @@ from serial import Serial, SerialException # It tries a pre-set list of baud rates. All these baud rates are validated by requesting the temperature a few times # and checking if the results make sense. If getResult() is not None, it was able to find a correct baud rate. class AutoDetectBaudJob(Job): - def __init__(self, serial_port): + def __init__(self, serial_port: int) -> None: super().__init__() self._serial_port = serial_port self._all_baud_rates = [115200, 250000, 230400, 57600, 38400, 19200, 9600] - def run(self): + def run(self) -> None: Logger.log("d", "Auto detect baud rate started.") wait_response_timeouts = [3, 15, 30] wait_bootloader_times = [1.5, 5, 15] @@ -32,7 +33,7 @@ class AutoDetectBaudJob(Job): try: programmer.connect(self._serial_port) serial = programmer.leaveISP() - except: + except ispBase.IspError: programmer.close() for retry in range(tries): @@ -58,7 +59,7 @@ class AutoDetectBaudJob(Job): # We already have a serial connection, just change the baud rate. try: serial.baudrate = baud_rate - except: + except ValueError: continue sleep(wait_bootloader) # Ensure that we are not talking to the boot loader. 1.5 seconds seems to be the magic number successful_responses = 0 @@ -81,5 +82,5 @@ class AutoDetectBaudJob(Job): return serial.write(b"M105\n") - sleep(15) # Give the printer some time to init and try again. + sleep(15) # Give the printer some time to init and try again. self.setResult(None) # Unable to detect the correct baudrate. From c1c5eb221913dd7ce7538ef9d4120f7b69866729 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Wed, 28 Nov 2018 09:44:37 +0100 Subject: [PATCH 140/146] Rename the properties to quickly identify that they are a boolean Contributes to CURA-5984. --- resources/qml/Toolbar.qml | 8 ++++---- resources/qml/ToolbarButton.qml | 12 ++++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/resources/qml/Toolbar.qml b/resources/qml/Toolbar.qml index 0207c8ec49..d16f949014 100644 --- a/resources/qml/Toolbar.qml +++ b/resources/qml/Toolbar.qml @@ -61,8 +61,8 @@ Item checked: model.active enabled: model.enabled && UM.Selection.hasSelection && UM.Controller.toolsEnabled - topElement: toolsModel.getItem(0).id == model.id - bottomElement: toolsModel.getItem(toolsModel.rowCount() - 1).id == model.id + isTopElement: toolsModel.getItem(0).id == model.id + isBottomElement: toolsModel.getItem(toolsModel.rowCount() - 1).id == model.id toolItem: UM.RecolorImage { @@ -137,8 +137,8 @@ Item delegate: ExtruderButton { extruder: model - topElement: extrudersModel.getItem(0).id == model.id - bottomElement: extrudersModel.getItem(extrudersModel.rowCount() - 1).id == model.id + isTopElement: extrudersModel.getItem(0).id == model.id + isBottomElement: extrudersModel.getItem(extrudersModel.rowCount() - 1).id == model.id } } } diff --git a/resources/qml/ToolbarButton.qml b/resources/qml/ToolbarButton.qml index b5e5aab475..9e81939ba2 100644 --- a/resources/qml/ToolbarButton.qml +++ b/resources/qml/ToolbarButton.qml @@ -12,8 +12,12 @@ Button id: base property alias toolItem: contentItemLoader.sourceComponent - property bool topElement: false - property bool bottomElement: false + + // These two properties indicate whether the toolbar button is at the top of the toolbar column or at the bottom. + // If it is somewhere in the middle, then both has to be false. If there is only one element in the column, then + // both properties have to be set to true. This is used to create a rounded corner. + property bool isTopElement: false + property bool isBottomElement: false hoverEnabled: true @@ -49,7 +53,7 @@ Button top: parent.top } height: parent.radius - color: base.topElement ? "transparent" : parent.color + color: base.isTopElement ? "transparent" : parent.color } Rectangle @@ -62,7 +66,7 @@ Button bottom: parent.bottom } height: parent.radius - color: base.bottomElement ? "transparent" : parent.color + color: base.isBottomElement ? "transparent" : parent.color } Rectangle From bfebb33123f34894887719f114c942485a48b540 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 28 Nov 2018 10:28:16 +0100 Subject: [PATCH 141/146] Code style CURA-5984 Co-Authored-By: diegopradogesto --- resources/qml/Toolbar.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/Toolbar.qml b/resources/qml/Toolbar.qml index d16f949014..07522dd535 100644 --- a/resources/qml/Toolbar.qml +++ b/resources/qml/Toolbar.qml @@ -66,7 +66,7 @@ Item toolItem: UM.RecolorImage { - source: (UM.Theme.getIcon(model.icon) != "") ? UM.Theme.getIcon(model.icon) : "file:///" + model.location + "/" + model.icon + source: UM.Theme.getIcon(model.icon) != "" ? UM.Theme.getIcon(model.icon) : "file:///" + model.location + "/" + model.icon color: UM.Theme.getColor("toolbar_button_text") sourceSize: UM.Theme.getSize("button_icon") From 454b47e3a0e7c9d50a5e8ebad5933182872f0782 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 28 Nov 2018 10:29:35 +0100 Subject: [PATCH 142/146] Change visibility behavior of the rectangle CURA-5984 Co-Authored-By: diegopradogesto --- resources/qml/ToolbarButton.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/qml/ToolbarButton.qml b/resources/qml/ToolbarButton.qml index 9e81939ba2..90e9160d3d 100644 --- a/resources/qml/ToolbarButton.qml +++ b/resources/qml/ToolbarButton.qml @@ -66,7 +66,8 @@ Button bottom: parent.bottom } height: parent.radius - color: base.isBottomElement ? "transparent" : parent.color + color: parent.color + visible: base.isBottomElement } Rectangle From d0da70a7eea8c6991afebc5488e7a2c39fae9ab1 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 28 Nov 2018 10:29:53 +0100 Subject: [PATCH 143/146] Change visibility of the rectangle CURA-5984 Co-Authored-By: diegopradogesto --- resources/qml/ToolbarButton.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/qml/ToolbarButton.qml b/resources/qml/ToolbarButton.qml index 90e9160d3d..157c6a34ac 100644 --- a/resources/qml/ToolbarButton.qml +++ b/resources/qml/ToolbarButton.qml @@ -53,7 +53,8 @@ Button top: parent.top } height: parent.radius - color: base.isTopElement ? "transparent" : parent.color + color: parent.color + visible: base.isTopElement } Rectangle From 709750c9e2c4fac776651039ac56055714056276 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Wed, 28 Nov 2018 10:39:32 +0100 Subject: [PATCH 144/146] Add global quality to the Anycubic i3 Mega Fixes #4876. --- .../quality/anycubic_i3_mega/anycubic_i3_mega_draft.inst.cfg | 1 + .../quality/anycubic_i3_mega/anycubic_i3_mega_high.inst.cfg | 1 + .../quality/anycubic_i3_mega/anycubic_i3_mega_normal.inst.cfg | 1 + 3 files changed, 3 insertions(+) diff --git a/resources/quality/anycubic_i3_mega/anycubic_i3_mega_draft.inst.cfg b/resources/quality/anycubic_i3_mega/anycubic_i3_mega_draft.inst.cfg index bb47f68574..e94b9f01d1 100644 --- a/resources/quality/anycubic_i3_mega/anycubic_i3_mega_draft.inst.cfg +++ b/resources/quality/anycubic_i3_mega/anycubic_i3_mega_draft.inst.cfg @@ -8,6 +8,7 @@ setting_version = 5 type = quality quality_type = draft weight = 0 +global_quality = True [values] acceleration_enabled = True diff --git a/resources/quality/anycubic_i3_mega/anycubic_i3_mega_high.inst.cfg b/resources/quality/anycubic_i3_mega/anycubic_i3_mega_high.inst.cfg index a3ae98deba..c8c4bf9a81 100644 --- a/resources/quality/anycubic_i3_mega/anycubic_i3_mega_high.inst.cfg +++ b/resources/quality/anycubic_i3_mega/anycubic_i3_mega_high.inst.cfg @@ -8,6 +8,7 @@ setting_version = 5 type = quality quality_type = high weight = 2 +global_quality = True [values] acceleration_enabled = True diff --git a/resources/quality/anycubic_i3_mega/anycubic_i3_mega_normal.inst.cfg b/resources/quality/anycubic_i3_mega/anycubic_i3_mega_normal.inst.cfg index 13846b9702..399c3ebc55 100644 --- a/resources/quality/anycubic_i3_mega/anycubic_i3_mega_normal.inst.cfg +++ b/resources/quality/anycubic_i3_mega/anycubic_i3_mega_normal.inst.cfg @@ -8,6 +8,7 @@ setting_version = 5 type = quality quality_type = normal weight = 1 +global_quality = True [values] acceleration_enabled = True From bfa2ff5f5ed84a06460f29a79f1b5cd0e1979187 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 28 Nov 2018 10:46:07 +0100 Subject: [PATCH 145/146] Invert visibility of bottom & topsquare It got derped. --- resources/qml/ToolbarButton.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/qml/ToolbarButton.qml b/resources/qml/ToolbarButton.qml index 157c6a34ac..adff73fb7c 100644 --- a/resources/qml/ToolbarButton.qml +++ b/resources/qml/ToolbarButton.qml @@ -54,7 +54,7 @@ Button } height: parent.radius color: parent.color - visible: base.isTopElement + visible: !base.isTopElement } Rectangle @@ -68,7 +68,7 @@ Button } height: parent.radius color: parent.color - visible: base.isBottomElement + visible: !base.isBottomElement } Rectangle From 5ed2acadd09fa062553f0fc4fddd2a79610e1142 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 28 Nov 2018 11:09:34 +0100 Subject: [PATCH 146/146] Re-add wizard_progress size I was a bit over enthosiastic deleting it as it was still used for the UMO first run wizard --- resources/themes/cura-light/theme.json | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index d2358e36ff..dfad5cfd17 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -470,6 +470,7 @@ "modal_window_minimum": [60.0, 45], "license_window_minimum": [45, 45], + "wizard_progress": [10.0, 0.0], "message": [30.0, 5.0], "message_close": [1, 1],

yIHgERaU=JNy7=q=;-jo7etgJi>{^i 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­1b1ri#7y-f4`n@X-6bylLTqFUm5RTM1i+O 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