diff --git a/Jenkinsfile b/Jenkinsfile index 8837fdf487..4f755dcae2 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -14,6 +14,7 @@ parallel_nodes(['linux && cura', 'windows && cura']) { catchError { stage('Pre Checks') { if (isUnix()) { + // Check shortcut keys try { sh """ echo 'Check for duplicate shortcut keys in all translation files.' @@ -22,6 +23,16 @@ parallel_nodes(['linux && cura', 'windows && cura']) { } catch(e) { currentBuild.result = "UNSTABLE" } + + // Check setting visibilities + try { + sh """ + echo 'Check for duplicate shortcut keys in all translation files.' + ${env.CURA_ENVIRONMENT_PATH}/master/bin/python3 scripts/check_setting_visibility.py + """ + } catch(e) { + currentBuild.result = "UNSTABLE" + } } } diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 243a9b5402..a94814502e 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -13,6 +13,7 @@ from PyQt5.QtGui import QColor, QIcon from PyQt5.QtWidgets import QMessageBox from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType +from UM.PluginError import PluginNotFoundError from UM.Scene.SceneNode import SceneNode from UM.Scene.Camera import Camera from UM.Math.Vector import Vector @@ -82,6 +83,7 @@ from cura.Settings.SettingInheritanceManager import SettingInheritanceManager from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager from cura.Machines.VariantManager import VariantManager +from plugins.SliceInfoPlugin.SliceInfo import SliceInfo from .SingleInstance import SingleInstance from .AutoSave import AutoSave @@ -113,7 +115,6 @@ from UM.FlameProfiler import pyqtSlot if TYPE_CHECKING: - from plugins.SliceInfoPlugin.SliceInfo import SliceInfo from cura.Machines.MaterialManager import MaterialManager from cura.Machines.QualityManager import QualityManager from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer @@ -1710,7 +1711,11 @@ class CuraApplication(QtApplication): @pyqtSlot() def showMoreInformationDialogForAnonymousDataCollection(self): - cast(SliceInfo, self._plugin_registry.getPluginObject("SliceInfoPlugin")).showMoreInfoDialog() + try: + slice_info = cast(SliceInfo, self._plugin_registry.getPluginObject("SliceInfoPlugin")) + slice_info.showMoreInfoDialog() + except PluginNotFoundError: + Logger.log("w", "Plugin SliceInfo was not found, so not able to show the info dialog.") def addSidebarCustomMenuItem(self, menu_item: dict) -> None: self._sidebar_custom_menu_items.append(menu_item) diff --git a/resources/setting_visibility/expert.cfg b/resources/setting_visibility/expert.cfg index 0ca2cbab70..437790ef74 100644 --- a/resources/setting_visibility/expert.cfg +++ b/resources/setting_visibility/expert.cfg @@ -110,7 +110,6 @@ 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 @@ -360,7 +359,6 @@ 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 diff --git a/scripts/check_setting_visibility.py b/scripts/check_setting_visibility.py new file mode 100755 index 0000000000..8fb5d5b293 --- /dev/null +++ b/scripts/check_setting_visibility.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +# +# This script checks the correctness of the list of visibility settings +# +import collections +import configparser +import json +import os +import sys +from typing import Any, Dict, List + +# Directory where this python file resides +SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) + + +# +# This class +# +class SettingVisibilityInspection: + + def __init__(self) -> None: + # The order of settings type. If the setting is in basic list then it also should be in expert + self._setting_visibility_order = ["basic", "advanced", "expert"] + + # This is dictionary with categories as keys and all setting keys as values. + self.all_settings_keys = {} # type: Dict[str, List[str]] + + # Load all Cura setting keys from the given fdmprinter.json file + def loadAllCuraSettingKeys(self, fdmprinter_json_path: str) -> None: + with open(fdmprinter_json_path, "r", encoding = "utf-8") as f: + json_data = json.load(f) + + # Get all settings keys in each category + for key, data in json_data["settings"].items(): # top level settings are categories + if "type" in data and data["type"] == "category": + self.all_settings_keys[key] = [] + self._flattenSettings(data["children"], key) # actual settings are children of top level category-settings + + def _flattenSettings(self, settings: Dict[str, str], category: str) -> None: + for key, setting in settings.items(): + if "type" in setting and setting["type"] != "category": + self.all_settings_keys[category].append(key) + + if "children" in setting: + self._flattenSettings(setting["children"], category) + + # Loads the given setting visibility file and returns a dict with categories as keys and a list of setting keys as + # values. + def _loadSettingVisibilityConfigFile(self, file_name: str) -> Dict[str, List[str]]: + with open(file_name, "r", encoding = "utf-8") as f: + parser = configparser.ConfigParser(allow_no_value = True) + parser.read_file(f) + + data_dict = {} + for category, option_dict in parser.items(): + if category in (parser.default_section, "general"): + continue + + data_dict[category] = [] + for key in option_dict: + data_dict[category].append(key) + + return data_dict + + def validateSettingsVisibility(self, setting_visibility_files: Dict[str, str]) -> Dict[str, Dict[str, Any]]: + # First load all setting visibility files into the dict "setting_visibility_dict" in the following structure: + # -> -> + # "basic" -> "info" + setting_visibility_dict = {} # type: Dict[str, Dict[str, List[str]]] + for visibility_name, file_path in setting_visibility_files.items(): + setting_visibility_dict[visibility_name] = self._loadSettingVisibilityConfigFile(file_path) + + # The result is in the format: + # -> dict + # "basic" -> "file_name": "basic.cfg" + # "is_valid": True / False + # "invalid_categories": List[str] + # "invalid_settings": Dict[category -> List[str]] + # "missing_categories_from_previous": List[str] + # "missing_settings_from_previous": Dict[category -> List[str]] + all_result_dict = dict() # type: Dict[str, Dict[str, Any]] + + previous_result = None + previous_visibility_dict = None + is_all_valid = True + for visibility_name in self._setting_visibility_order: + invalid_categories = [] + invalid_settings = collections.defaultdict(list) + + this_visibility_dict = setting_visibility_dict[visibility_name] + # Check if categories and keys exist at all + for category, key_list in this_visibility_dict.items(): + if category not in self.all_settings_keys: + invalid_categories.append(category) + continue # If this category doesn't exist at all, not need to check for details + + for key in key_list: + if key not in self.all_settings_keys[category]: + invalid_settings[category].append(key) + + is_settings_valid = len(invalid_categories) == 0 and len(invalid_settings) == 0 + file_path = setting_visibility_files[visibility_name] + result_dict = {"file_name": os.path.basename(file_path), + "is_valid": is_settings_valid, + "invalid_categories": invalid_categories, + "invalid_settings": invalid_settings, + "missing_categories_from_previous": list(), + "missing_settings_from_previous": dict(), + } + + # If this is not the first item in the list, check if the settings are defined in the previous + # visibility file. + # A visibility with more details SHOULD add more settings. It SHOULD NOT remove any settings defined + # in the less detailed visibility. + if previous_visibility_dict is not None: + missing_categories_from_previous = [] + missing_settings_from_previous = collections.defaultdict(list) + + for prev_category, prev_key_list in previous_visibility_dict.items(): + # Skip the categories that are invalid + if prev_category in previous_result["invalid_categories"]: + continue + if prev_category not in this_visibility_dict: + missing_categories_from_previous.append(prev_category) + continue + + this_key_list = this_visibility_dict[prev_category] + for key in prev_key_list: + # Skip the settings that are invalid + if key in previous_result["invalid_settings"][prev_category]: + continue + + if key not in this_key_list: + missing_settings_from_previous[prev_category].append(key) + + result_dict["missing_categories_from_previous"] = missing_categories_from_previous + result_dict["missing_settings_from_previous"] = missing_settings_from_previous + is_settings_valid = len(missing_categories_from_previous) == 0 and len(missing_settings_from_previous) == 0 + result_dict["is_valid"] = result_dict["is_valid"] and is_settings_valid + + # Update the complete result dict + all_result_dict[visibility_name] = result_dict + previous_result = result_dict + previous_visibility_dict = this_visibility_dict + + is_all_valid = is_all_valid and result_dict["is_valid"] + + all_result_dict["all_results"] = {"is_valid": is_all_valid} + + return all_result_dict + + def printResults(self, all_result_dict: Dict[str, Dict[str, Any]]) -> None: + print("") + print("Setting Visibility Check Results:") + + prev_visibility_name = None + for visibility_name in self._setting_visibility_order: + if visibility_name not in all_result_dict: + continue + + result_dict = all_result_dict[visibility_name] + print("=============================") + result_str = "OK" if result_dict["is_valid"] else "INVALID" + print("[%s] : [%s] : %s" % (visibility_name, result_dict["file_name"], result_str)) + + if result_dict["is_valid"]: + continue + + # Print details of invalid settings + if result_dict["invalid_categories"]: + print("It has the following non-existing CATEGORIES:") + for category in result_dict["invalid_categories"]: + print(" - [%s]" % category) + + if result_dict["invalid_settings"]: + print("") + print("It has the following non-existing SETTINGS:") + for category, key_list in result_dict["invalid_settings"].items(): + for key in key_list: + print(" - [%s / %s]" % (category, key)) + + if prev_visibility_name is not None: + if result_dict["missing_categories_from_previous"]: + print("") + print("The following CATEGORIES are defined in the previous visibility [%s] but not here:" % prev_visibility_name) + for category in result_dict["missing_categories_from_previous"]: + print(" - [%s]" % category) + + if result_dict["missing_settings_from_previous"]: + print("") + print("The following SETTINGS are defined in the previous visibility [%s] but not here:" % prev_visibility_name) + for category, key_list in result_dict["missing_settings_from_previous"].items(): + for key in key_list: + print(" - [%s / %s]" % (category, key)) + + print("") + prev_visibility_name = visibility_name + + +# +# Returns a dictionary of setting visibility .CFG files in the given search directory. +# The dict has the name of the visibility type as the key (such as "basic", "advanced", "expert"), and +# the actual file path (absolute path). +# +def getAllSettingVisiblityFiles(search_dir: str) -> Dict[str, str]: + visibility_file_dict = dict() + extension = ".cfg" + for file_name in os.listdir(search_dir): + file_path = os.path.join(search_dir, file_name) + + # Only check files that has the .cfg extension + if not os.path.isfile(file_path): + continue + if not file_path.endswith(extension): + continue + + base_filename = os.path.basename(file_name)[:-len(extension)] + visibility_file_dict[base_filename] = file_path + return visibility_file_dict + + +def main() -> None: + setting_visibility_files_dir = os.path.abspath(os.path.join(SCRIPT_DIR, "..", "resources", "setting_visibility")) + fdmprinter_def_path = os.path.abspath(os.path.join(SCRIPT_DIR, "..", "resources", "definitions", "fdmprinter.def.json")) + + setting_visibility_files_dict = getAllSettingVisiblityFiles(setting_visibility_files_dir) + + inspector = SettingVisibilityInspection() + inspector.loadAllCuraSettingKeys(fdmprinter_def_path) + + check_result = inspector.validateSettingsVisibility(setting_visibility_files_dict) + is_result_valid = check_result["all_results"]["is_valid"] + inspector.printResults(check_result) + + sys.exit(0 if is_result_valid else 1) + + +if __name__ == "__main__": + main()