Browse Source

Merge pull request #3218 from Ultimaker/CURA-3710_setting_visibility_preset

CURA-3710 Add preset setting visibility files
Ian Paschal 7 years ago
parent
commit
f627c97a92

+ 105 - 53
cura/CuraApplication.py

@@ -1,6 +1,7 @@
-# Copyright (c) 2017 Ultimaker B.V.
-# Copyright (c) 2017 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
+#Type hinting.
+from typing import Dict
 from PyQt5.QtNetwork import QLocalServer
 from PyQt5.QtNetwork import QLocalSocket
 
@@ -87,6 +88,7 @@ from PyQt5.QtGui import QColor, QIcon
 from PyQt5.QtWidgets import QMessageBox
 from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
 
+from configparser import ConfigParser
 import sys
 import os.path
 import numpy
@@ -95,6 +97,7 @@ import os
 import argparse
 import json
 
+
 numpy.seterr(all="ignore")
 
 MYPY = False
@@ -348,57 +351,19 @@ class CuraApplication(QtApplication):
 
         preferences.setDefault("local_file/last_used_type", "text/x-gcode")
 
-        preferences.setDefault("general/visible_settings", """
-            machine_settings
-            resolution
-                layer_height
-            shell
-                wall_thickness
-                top_bottom_thickness
-                z_seam_x
-                z_seam_y
-            infill
-                infill_sparse_density
-                gradual_infill_steps
-            material
-                material_print_temperature
-                material_bed_temperature
-                material_diameter
-                material_flow
-                retraction_enable
-            speed
-                speed_print
-                speed_travel
-                acceleration_print
-                acceleration_travel
-                jerk_print
-                jerk_travel
-            travel
-            cooling
-                cool_fan_enabled
-            support
-                support_enable
-                support_extruder_nr
-                support_type
-            platform_adhesion
-                adhesion_type
-                adhesion_extruder_nr
-                brim_width
-                raft_airgap
-                layer_0_z_overlap
-                raft_surface_layers
-            dual
-                prime_tower_enable
-                prime_tower_size
-                prime_tower_position_x
-                prime_tower_position_y
-            meshfix
-            blackmagic
-                print_sequence
-                infill_mesh
-                cutting_mesh
-            experimental
-        """.replace("\n", ";").replace(" ", ""))
+        setting_visibily_preset_names = self.getVisibilitySettingPresetTypes()
+        preferences.setDefault("general/visible_settings_preset", setting_visibily_preset_names)
+
+        preset_setting_visibility_choice = Preferences.getInstance().getValue("general/preset_setting_visibility_choice")
+
+        default_preset_visibility_group_name = "Basic"
+        if preset_setting_visibility_choice == "" or preset_setting_visibility_choice is None:
+            if preset_setting_visibility_choice not in setting_visibily_preset_names:
+                preset_setting_visibility_choice = default_preset_visibility_group_name
+
+        visible_settings = self.getVisibilitySettingPreset(settings_preset_name = preset_setting_visibility_choice)
+        preferences.setDefault("general/visible_settings", visible_settings)
+        preferences.setDefault("general/preset_setting_visibility_choice", preset_setting_visibility_choice)
 
         self.applicationShuttingDown.connect(self.saveSettings)
         self.engineCreatedSignal.connect(self._onEngineCreated)
@@ -410,6 +375,93 @@ class CuraApplication(QtApplication):
 
         self.getCuraSceneController().setActiveBuildPlate(0)  # Initialize
 
+    @pyqtSlot(str, result = str)
+    def getVisibilitySettingPreset(self, settings_preset_name) -> str:
+        result = self._loadPresetSettingVisibilityGroup(settings_preset_name)
+        formatted_preset_settings = self._serializePresetSettingVisibilityData(result)
+
+        return formatted_preset_settings
+
+    ## Serialise the given preset setting visibitlity group dictionary into a string which is concatenated by ";"
+    #
+    def _serializePresetSettingVisibilityData(self, settings_data: dict) -> str:
+        result_string = ""
+
+        for key in settings_data:
+            result_string += key + ";"
+            for value in settings_data[key]:
+                result_string += value + ";"
+
+        return result_string
+
+    ## Load the preset setting visibility group with the given name
+    #
+    def _loadPresetSettingVisibilityGroup(self, visibility_preset_name) -> Dict[str, str]:
+        preset_dir = Resources.getPath(Resources.PresetSettingVisibilityGroups)
+
+        result = {}
+        right_preset_found = False
+
+        for item in os.listdir(preset_dir):
+            file_path = os.path.join(preset_dir, item)
+            if not os.path.isfile(file_path):
+                continue
+
+            parser = ConfigParser(allow_no_value = True)  # accept options without any value,
+
+            try:
+                parser.read([file_path])
+
+                if not parser.has_option("general", "name"):
+                    continue
+
+                if parser["general"]["name"] == visibility_preset_name:
+                    right_preset_found = True
+                    for section in parser.sections():
+                        if section == 'general':
+                            continue
+                        else:
+                            section_settings = []
+                            for option in parser[section].keys():
+                                section_settings.append(option)
+
+                            result[section] = section_settings
+
+                if right_preset_found:
+                    break
+
+            except Exception as e:
+                Logger.log("e", "Failed to load setting visibility preset %s: %s", file_path, str(e))
+
+        return result
+
+    ## Check visibility setting preset folder and returns available types
+    #
+    def getVisibilitySettingPresetTypes(self):
+        preset_dir = Resources.getPath(Resources.PresetSettingVisibilityGroups)
+        result = {}
+
+        for item in os.listdir(preset_dir):
+            file_path = os.path.join(preset_dir, item)
+            if not os.path.isfile(file_path):
+                continue
+
+            parser = ConfigParser(allow_no_value=True)  # accept options without any value,
+
+            try:
+                parser.read([file_path])
+
+                if not parser.has_option("general", "name") and not parser.has_option("general", "weight"):
+                    continue
+
+                result[parser["general"]["weight"]] = parser["general"]["name"]
+
+            except Exception as e:
+                Logger.log("e", "Failed to load setting preset %s: %s", file_path, str(e))
+
+        return result
+
+
     def _onEngineCreated(self):
         self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
 

+ 1 - 0
plugins/3MFReader/ThreeMFWorkspaceReader.py

@@ -453,6 +453,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
             Logger.log("w", "Workspace did not contain visible settings. Leaving visibility unchanged")
         else:
             global_preferences.setValue("general/visible_settings", visible_settings)
+            global_preferences.setValue("general/preset_setting_visibility_choice", "Custom")
 
         categories_expanded = temp_preferences.getValue("cura/categories_expanded")
         if categories_expanded is None:

+ 254 - 0
resources/preset_setting_visibility_groups/advanced.cfg

@@ -0,0 +1,254 @@
+[general]
+name = Advanced
+weight = 2
+
+[machine_settings]
+
+[resolution]
+layer_height
+layer_height_0
+line_width
+
+[shell]
+wall_extruder_nr
+wall_thickness
+wall_0_wipe_dist
+roofing_extruder_nr
+roofing_layer_count
+top_bottom_extruder_nr
+top_bottom_thickness
+top_bottom_pattern
+top_bottom_pattern_0
+skin_angles
+wall_0_inset
+optimize_wall_printing_order
+outer_inset_first
+alternate_extra_perimeter
+travel_compensate_overlapping_walls_enabled
+fill_perimeter_gaps
+filter_out_tiny_gaps
+fill_outline_gaps
+xy_offset
+xy_offset_layer_0
+z_seam_type
+z_seam_x
+z_seam_y
+z_seam_corner
+z_seam_relative
+skin_no_small_gaps_heuristic
+skin_outline_count
+ironing_enabled
+ironing_only_highest_layer
+ironing_pattern
+ironing_line_spacing
+ironing_flow
+ironing_inset
+speed_ironing
+acceleration_ironing
+jerk_ironing
+
+[infill]
+infill_extruder_nr
+infill_sparse_density
+infill_pattern
+zig_zaggify_infill
+infill_angles
+infill_offset_x
+infill_offset_y
+sub_div_rad_add
+infill_overlap
+skin_overlap
+infill_wipe_dist
+infill_sparse_thickness
+gradual_infill_steps
+gradual_infill_step_height
+infill_before_walls
+min_infill_area
+skin_preshrink
+expand_skins_expand_distance
+max_skin_angle_for_expansion
+
+[material]
+default_material_print_temperature
+material_print_temperature
+material_print_temperature_layer_0
+material_initial_print_temperature
+material_final_print_temperature
+material_extrusion_cool_down_speed
+default_material_bed_temperature
+material_bed_temperature
+material_bed_temperature_layer_0
+material_diameter
+material_adhesion_tendency
+material_surface_energy
+material_flow
+retraction_enable
+retract_at_layer_change
+retraction_amount
+retraction_speed
+retraction_extra_prime_amount
+retraction_min_travel
+retraction_count_max
+retraction_extrusion_window
+material_standby_temperature
+switch_extruder_retraction_amount
+switch_extruder_retraction_speeds
+
+[speed]
+speed_print
+speed_travel
+speed_layer_0
+skirt_brim_speed
+max_feedrate_z_override
+speed_slowdown_layers
+speed_equalize_flow_enabled
+speed_equalize_flow_max
+acceleration_enabled
+acceleration_print
+acceleration_travel
+acceleration_layer_0
+acceleration_skirt_brim
+jerk_enabled
+jerk_print
+jerk_travel
+jerk_layer_0
+jerk_skirt_brim
+
+[travel]
+retraction_combing
+travel_retract_before_outer_wall
+travel_avoid_other_parts
+travel_avoid_distance
+start_layers_at_same_position
+layer_start_x
+layer_start_y
+retraction_hop_enabled
+retraction_hop_only_when_collides
+retraction_hop
+retraction_hop_after_extruder_switch
+
+[cooling]
+cool_fan_enabled
+cool_fan_speed
+cool_min_layer_time_fan_speed_max
+cool_fan_speed_0
+cool_fan_full_at_height
+cool_min_layer_time
+cool_min_speed
+cool_lift_head
+
+[support]
+support_enable
+support_extruder_nr
+support_type
+support_angle
+support_pattern
+support_connect_zigzags
+support_infill_rate
+support_z_distance
+support_xy_distance
+support_xy_overrides_z
+support_xy_distance_overhang
+support_bottom_stair_step_height
+support_bottom_stair_step_width
+support_join_distance
+support_offset
+support_infill_sparse_thickness
+gradual_support_infill_steps
+gradual_support_infill_step_height
+support_interface_enable
+support_interface_height
+support_interface_skip_height
+support_interface_density
+support_interface_pattern
+support_use_towers
+support_tower_diameter
+support_minimal_diameter
+support_tower_roof_angle
+support_mesh_drop_down
+
+[platform_adhesion]
+prime_blob_enable
+extruder_prime_pos_x
+extruder_prime_pos_y
+adhesion_type
+adhesion_extruder_nr
+skirt_line_count
+skirt_gap
+skirt_brim_minimal_length
+brim_width
+brim_outside_only
+raft_margin
+raft_smoothing
+raft_airgap
+layer_0_z_overlap
+raft_surface_layers
+raft_surface_thickness
+raft_surface_line_width
+raft_surface_line_spacing
+raft_interface_thickness
+raft_interface_line_width
+raft_interface_line_spacing
+raft_base_thickness
+raft_base_line_width
+raft_base_line_spacing
+raft_speed
+raft_acceleration
+raft_jerk
+raft_fan_speed
+
+[dual]
+prime_tower_enable
+prime_tower_size
+prime_tower_min_volume
+prime_tower_position_x
+prime_tower_position_y
+prime_tower_flow
+prime_tower_wipe_enabled
+dual_pre_wipe
+prime_tower_purge_volume
+ooze_shield_enabled
+ooze_shield_angle
+ooze_shield_dist
+
+[meshfix]
+meshfix_union_all
+meshfix_union_all_remove_holes
+meshfix_extensive_stitching
+meshfix_keep_open_polygons
+multiple_mesh_overlap
+carve_multiple_volumes
+alternate_carve_order
+remove_empty_first_layers
+
+[blackmagic]
+print_sequence
+infill_mesh
+infill_mesh_order
+cutting_mesh
+mold_enabled
+mold_width
+mold_roof_height
+mold_angle
+support_mesh
+anti_overhang_mesh
+magic_mesh_surface_mode
+magic_spiralize
+smooth_spiralized_contours
+relative_extrusion
+
+[experimental]
+infill_enable_travel_optimization
+material_flow_dependent_temperature
+material_flow_temp_graph
+meshfix_maximum_resolution
+roofing_angles
+roofing_pattern
+slicing_tolerance
+support_tree_angle
+support_tree_branch_diameter
+support_tree_branch_diameter_angle
+support_tree_branch_distance
+support_tree_collision_resolution
+support_tree_enable
+support_tree_wall_thickness

+ 66 - 0
resources/preset_setting_visibility_groups/basic.cfg

@@ -0,0 +1,66 @@
+[general]
+name = Basic
+weight = 1
+
+[machine_settings]
+
+[resolution]
+layer_height
+
+[shell]
+wall_thickness
+top_bottom_thickness
+z_seam_x
+z_seam_y
+
+[infill]
+infill_sparse_density
+gradual_infill_steps
+
+[material]
+material_print_temperature
+material_bed_temperature
+material_diameter
+material_flow
+retraction_enable
+
+[speed]
+speed_print
+speed_travel
+acceleration_print
+acceleration_travel
+jerk_print
+jerk_travel
+
+[travel]
+
+[cooling]
+cool_fan_enabled
+
+[support]
+support_enable
+support_extruder_nr
+support_type
+
+[platform_adhesion]
+adhesion_type
+adhesion_extruder_nr
+brim_width
+raft_airgap
+layer_0_z_overlap
+raft_surface_layers
+
+[dual]
+prime_tower_enable
+prime_tower_size
+prime_tower_position_x
+prime_tower_position_y
+
+[meshfix]
+
+[blackmagic]
+print_sequence
+infill_mesh
+cutting_mesh
+
+[experimental]

+ 285 - 0
resources/preset_setting_visibility_groups/expert.cfg

@@ -0,0 +1,285 @@
+[general]
+name = Expert
+weight = 3
+
+[machine_settings]
+
+[resolution]
+layer_height
+layer_height_0
+line_width
+
+[shell]
+wall_extruder_nr
+wall_thickness
+wall_0_wipe_dist
+roofing_extruder_nr
+roofing_layer_count
+top_bottom_extruder_nr
+top_bottom_thickness
+top_bottom_pattern
+top_bottom_pattern_0
+skin_angles
+wall_0_inset
+optimize_wall_printing_order
+outer_inset_first
+alternate_extra_perimeter
+travel_compensate_overlapping_walls_enabled
+fill_perimeter_gaps
+filter_out_tiny_gaps
+fill_outline_gaps
+xy_offset
+xy_offset_layer_0
+z_seam_type
+z_seam_x
+z_seam_y
+z_seam_corner
+z_seam_relative
+skin_no_small_gaps_heuristic
+skin_outline_count
+ironing_enabled
+ironing_only_highest_layer
+ironing_pattern
+ironing_line_spacing
+ironing_flow
+ironing_inset
+speed_ironing
+acceleration_ironing
+jerk_ironing
+
+[infill]
+infill_extruder_nr
+infill_sparse_density
+infill_pattern
+zig_zaggify_infill
+infill_angles
+infill_offset_x
+infill_offset_y
+sub_div_rad_add
+infill_overlap
+skin_overlap
+infill_wipe_dist
+infill_sparse_thickness
+gradual_infill_steps
+gradual_infill_step_height
+infill_before_walls
+min_infill_area
+skin_preshrink
+expand_skins_expand_distance
+max_skin_angle_for_expansion
+
+[material]
+default_material_print_temperature
+material_print_temperature
+material_print_temperature_layer_0
+material_initial_print_temperature
+material_final_print_temperature
+material_extrusion_cool_down_speed
+default_material_bed_temperature
+material_bed_temperature
+material_bed_temperature_layer_0
+material_diameter
+material_adhesion_tendency
+material_surface_energy
+material_flow
+retraction_enable
+retract_at_layer_change
+retraction_amount
+retraction_speed
+retraction_extra_prime_amount
+retraction_min_travel
+retraction_count_max
+retraction_extrusion_window
+material_standby_temperature
+switch_extruder_retraction_amount
+switch_extruder_retraction_speeds
+
+[speed]
+speed_print
+speed_travel
+speed_layer_0
+skirt_brim_speed
+max_feedrate_z_override
+speed_slowdown_layers
+speed_equalize_flow_enabled
+speed_equalize_flow_max
+acceleration_enabled
+acceleration_print
+acceleration_travel
+acceleration_layer_0
+acceleration_skirt_brim
+jerk_enabled
+jerk_print
+jerk_travel
+jerk_layer_0
+jerk_skirt_brim
+
+[travel]
+retraction_combing
+travel_retract_before_outer_wall
+travel_avoid_other_parts
+travel_avoid_distance
+start_layers_at_same_position
+layer_start_x
+layer_start_y
+retraction_hop_enabled
+retraction_hop_only_when_collides
+retraction_hop
+retraction_hop_after_extruder_switch
+
+[cooling]
+cool_fan_enabled
+cool_fan_speed
+cool_min_layer_time_fan_speed_max
+cool_fan_speed_0
+cool_fan_full_at_height
+cool_min_layer_time
+cool_min_speed
+cool_lift_head
+
+[support]
+support_enable
+support_extruder_nr
+support_type
+support_angle
+support_pattern
+support_connect_zigzags
+support_infill_rate
+support_z_distance
+support_xy_distance
+support_xy_overrides_z
+support_xy_distance_overhang
+support_bottom_stair_step_height
+support_bottom_stair_step_width
+support_join_distance
+support_offset
+support_infill_sparse_thickness
+gradual_support_infill_steps
+gradual_support_infill_step_height
+support_interface_enable
+support_interface_height
+support_interface_skip_height
+support_interface_density
+support_interface_pattern
+support_use_towers
+support_tower_diameter
+support_minimal_diameter
+support_tower_roof_angle
+support_mesh_drop_down
+
+[platform_adhesion]
+prime_blob_enable
+extruder_prime_pos_x
+extruder_prime_pos_y
+adhesion_type
+adhesion_extruder_nr
+skirt_line_count
+skirt_gap
+skirt_brim_minimal_length
+brim_width
+brim_outside_only
+raft_margin
+raft_smoothing
+raft_airgap
+layer_0_z_overlap
+raft_surface_layers
+raft_surface_thickness
+raft_surface_line_width
+raft_surface_line_spacing
+raft_interface_thickness
+raft_interface_line_width
+raft_interface_line_spacing
+raft_base_thickness
+raft_base_line_width
+raft_base_line_spacing
+raft_speed
+raft_acceleration
+raft_jerk
+raft_fan_speed
+
+[dual]
+prime_tower_enable
+prime_tower_size
+prime_tower_min_volume
+prime_tower_position_x
+prime_tower_position_y
+prime_tower_flow
+prime_tower_wipe_enabled
+dual_pre_wipe
+prime_tower_purge_volume
+ooze_shield_enabled
+ooze_shield_angle
+ooze_shield_dist
+
+[meshfix]
+meshfix_union_all
+meshfix_union_all_remove_holes
+meshfix_extensive_stitching
+meshfix_keep_open_polygons
+multiple_mesh_overlap
+carve_multiple_volumes
+alternate_carve_order
+remove_empty_first_layers
+
+[blackmagic]
+print_sequence
+infill_mesh
+infill_mesh_order
+cutting_mesh
+mold_enabled
+mold_width
+mold_roof_height
+mold_angle
+support_mesh
+anti_overhang_mesh
+magic_mesh_surface_mode
+magic_spiralize
+smooth_spiralized_contours
+relative_extrusion
+
+[experimental]
+support_skip_some_zags
+support_skip_zag_per_mm
+draft_shield_enabled
+draft_shield_dist
+draft_shield_height_limitation
+draft_shield_height
+conical_overhang_enabled
+conical_overhang_angle
+coasting_enable
+coasting_volume
+coasting_min_volume
+coasting_speed
+skin_alternate_rotation
+cross_infill_pocket_size
+cross_infill_apply_pockets_alternatingly
+spaghetti_infill_enabled
+spaghetti_infill_stepped
+spaghetti_max_infill_angle
+spaghetti_max_height
+spaghetti_inset
+spaghetti_flow
+spaghetti_infill_extra_volume
+support_conical_enabled
+support_conical_angle
+support_conical_min_width
+infill_hollow
+magic_fuzzy_skin_enabled
+magic_fuzzy_skin_thickness
+magic_fuzzy_skin_point_density
+flow_rate_max_extrusion_offset
+flow_rate_extrusion_offset_factor
+infill_enable_travel_optimization
+material_flow_dependent_temperature
+material_flow_temp_graph
+meshfix_maximum_resolution
+roofing_angles
+roofing_pattern
+slicing_tolerance
+support_tree_angle
+support_tree_branch_diameter
+support_tree_branch_diameter_angle
+support_tree_branch_distance
+support_tree_collision_resolution
+support_tree_enable
+support_tree_wall_thickness

+ 103 - 3
resources/qml/Preferences/SettingVisibilityPage.qml

@@ -24,6 +24,11 @@ UM.PreferencesPage
     function reset()
     {
         UM.Preferences.resetPreference("general/visible_settings")
+
+        // After calling this function update Setting visibility preset combobox.
+        // Reset should set "Basic" setting preset
+        visibilityPreset.setBasicPreset()
+
     }
     resetEnabled: true;
 
@@ -72,6 +77,9 @@ UM.PreferencesPage
                     {
                         definitionsModel.setAllVisible(false)
                     }
+
+                    // After change set "Custom" option
+                    visibilityPreset.currentIndex = visibilityPreset.model.count - 1
                 }
             }
         }
@@ -85,7 +93,8 @@ UM.PreferencesPage
                 top: parent.top
                 left: toggleVisibleSettings.right
                 leftMargin: UM.Theme.getSize("default_margin").width
-                right: parent.right
+                right: visibilityPreset.left
+                rightMargin: UM.Theme.getSize("default_margin").width
             }
 
             placeholderText: catalog.i18nc("@label:textbox", "Filter...")
@@ -93,6 +102,86 @@ UM.PreferencesPage
             onTextChanged: definitionsModel.filter = {"i18n_label": "*" + text}
         }
 
+        ComboBox
+        {
+            property int customOptionValue: 100
+
+            function setBasicPreset()
+            {
+                var index = 0
+                for(var i = 0; i < presetNamesList.count; ++i)
+                {
+                    if(model.get(i).text == "Basic")
+                    {
+                        index = i;
+                        break;
+                    }
+                }
+
+                visibilityPreset.currentIndex = index
+            }
+
+            id: visibilityPreset
+            width: 150
+            anchors
+            {
+                top: parent.top
+                right: parent.right
+            }
+
+            model: ListModel
+            {
+                id: presetNamesList
+                Component.onCompleted:
+                {
+                    // returned value is Dictionary  (Ex: {1:"Basic"}, The number 1 is the weight and sort by weight)
+                    var itemsDict = UM.Preferences.getValue("general/visible_settings_preset")
+                    var sorted = [];
+                    for(var key in itemsDict) {
+                        sorted[sorted.length] = key;
+                    }
+
+                    sorted.sort();
+                    for(var i = 0; i < sorted.length; i++) {
+                        presetNamesList.append({text: itemsDict[sorted[i]], value: i});
+                    }
+
+                    // By agreement lets "Custom" option will have value 100
+                    presetNamesList.append({text: "Custom", value: visibilityPreset.customOptionValue});
+                }
+            }
+
+            currentIndex:
+            {
+                // Load previously selected preset.
+                var text = UM.Preferences.getValue("general/preset_setting_visibility_choice");
+
+
+
+                var index = 0;
+                for(var i = 0; i < presetNamesList.count; ++i)
+                {
+                    if(model.get(i).text == text)
+                    {
+                        index = i;
+                        break;
+                    }
+                }
+                return index;
+            }
+
+            onActivated:
+            {
+                // TODO What to do if user is selected "Custom from Combobox" ?
+                if (model.get(index).text == "Custom")
+                    return
+
+                var newVisibleSettings = CuraApplication.getVisibilitySettingPreset(model.get(index).text)
+                UM.Preferences.setValue("general/visible_settings", newVisibleSettings)
+                UM.Preferences.setValue("general/preset_setting_visibility_choice", model.get(index).text)
+            }
+        }
+
         ScrollView
         {
             id: scrollView
@@ -162,7 +251,18 @@ UM.PreferencesPage
         {
             id: settingVisibilityItem;
 
-            UM.SettingVisibilityItem { }
+            UM.SettingVisibilityItem {
+
+                // after changing any visibility of settings, set the preset to the "Custom" option
+                visibilityChangeCallback : function()
+                {
+                    // If already "Custom" then don't do nothing
+                    if (visibilityPreset.currentIndex != visibilityPreset.model.count - 1)
+                    {
+                        visibilityPreset.currentIndex = visibilityPreset.model.count - 1
+                    }
+                }
+            }
         }
     }
-}
+}

+ 126 - 0
tools/check_preset_settings.py

@@ -0,0 +1,126 @@
+#!/usr/bin/env python
+import configparser
+import json
+import os
+import sys
+
+
+class PresetSettingsValidator:
+
+    def __init__(self, cura_dir: str):
+        self._cura_dir = os.path.abspath(cura_dir)
+        self._resource_dir = os.path.join(self._cura_dir, "resources")
+        self._definitions_dir = os.path.join(self._resource_dir, "definitions")
+        self._preset_settings_dir = os.path.join(self._resource_dir, "preset_setting_visibility_groups")
+
+        self._fdmprinter_def_path = os.path.join(self._definitions_dir, "fdmprinter.def.json")
+
+    def validate(self) -> bool:
+        """
+        Validates the preset settings files and returns True or False indicating whether there are invalid files.
+        """
+        if not os.path.isfile(self._fdmprinter_def_path):
+            raise FileNotFoundError("[%s] is not a file or doesn't exist, please make sure you have specified the correct cura directory [%s]." % (self._fdmprinter_def_path, self._cura_dir))
+
+        if not os.path.isdir(self._preset_settings_dir):
+            raise FileNotFoundError("[%s] is not a directory or doesn't exist, please make sure you have specified the correct cura directory [%s]." % (self._preset_settings_dir, self._cura_dir))
+
+        # parse the definition file
+        setting_tree_dict = self._parse_definition_file(self._fdmprinter_def_path)
+
+        has_invalid_files = False
+
+        # go through all the preset settings files
+        for root_dir, _, filenames in os.walk(self._preset_settings_dir):
+            for filename in filenames:
+                file_path = os.path.join(root_dir, filename)
+                print("Validating [%s] ..." % file_path)
+
+                incorrect_sections = []
+                incorrect_settings = {}
+
+                parser = configparser.ConfigParser(allow_no_value = True)
+                with open(file_path, "r", encoding = "utf-8") as f:
+                    parser.read_file(f)
+
+                for key in parser:
+                    # skip general
+                    if key in ("general", configparser.DEFAULTSECT):
+                        continue
+
+                    if key not in setting_tree_dict:
+                        incorrect_sections.append(key)
+                        continue
+
+                    for setting_key in parser[key]:
+                        if setting_key not in setting_tree_dict[key]:
+                            if setting_key not in incorrect_settings:
+                                incorrect_settings[setting_key] = {"seen_in": [],
+                                                                   "should_be_in": self._should_setting_be_in(setting_tree_dict, setting_key)}
+
+                            incorrect_settings[setting_key]["seen_in"].append(key)
+
+                # show results
+                print("==========================================")
+                if incorrect_sections or incorrect_settings:
+                    has_invalid_files = True
+                    print("[INVALID] [%s] is invalid, details below" % file_path)
+
+                    # show details
+                    for section_name in sorted(incorrect_sections):
+                        print(" -- section name [%s] is incorrect, please check fdmprinter.def.json." % section_name)
+
+                    for setting_name in sorted(incorrect_settings.keys()):
+                        details_dict = incorrect_settings[setting_name]
+                        msg = " -- setting [%s] is found in sections [%s], " % (setting_name, ", ".join(details_dict["seen_in"]))
+                        if details_dict["should_be_in"] is not None:
+                            msg += "but should be in section [%s] only." % details_dict["should_be_in"]
+                        else:
+                            msg += "but it cannot be found in fdmprinter.def.json"
+                        print(msg)
+
+                else:
+                    print("[%s] is valid" % file_path)
+                print("==========================================")
+
+        return not has_invalid_files
+
+    def _parse_definition_file(self, file_path: str):
+        with open(file_path, "r", encoding = "utf-8") as f:
+            def_dict = json.load(f, encoding = "utf-8")
+
+        tree_dict = {}
+        for key, item in def_dict.get("settings", {}).items():
+            setting_list = []
+            self._generate_tree(setting_list, item.get("children", {}))
+            tree_dict[key] = setting_list
+
+        return tree_dict
+
+    def _generate_tree(self, setting_list: list, setting_dict: dict):
+        for key, item in setting_dict.items():
+            setting_list.append(key)
+            if "children" in item:
+                self._generate_tree(setting_list, item["children"])
+
+    def _should_setting_be_in(self, setting_dict: dict, setting_name: str) -> str:
+        """
+        Check which section the given setting belongs to. Returns None if the setting cannot be found.
+        """
+        section_name = None
+        for key, setting_list in setting_dict.items():
+            if setting_name in setting_list:
+                section_name = key
+                break
+        return section_name
+
+
+if __name__ == "__main__":
+    script_dir = os.path.dirname(os.path.realpath(__file__))
+    cura_dir = os.path.abspath(os.path.join(script_dir, ".."))
+
+    validator = PresetSettingsValidator(cura_dir)
+    is_everything_validate = validator.validate()
+
+    ret_code = 0 if is_everything_validate else 1
+    sys.exit(ret_code)