mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-08-06 21:44:01 -06:00
Merge branch 'ui_rework_4_0' into cura4.0_header
This commit is contained in:
commit
54f32a1d82
10 changed files with 312 additions and 383 deletions
88
Jenkinsfile
vendored
88
Jenkinsfile
vendored
|
@ -1,8 +1,11 @@
|
|||
parallel_nodes(['linux && cura', 'windows && cura']) {
|
||||
timeout(time: 2, unit: "HOURS") {
|
||||
parallel_nodes(['linux && cura', 'windows && cura'])
|
||||
{
|
||||
timeout(time: 2, unit: "HOURS")
|
||||
{
|
||||
|
||||
// Prepare building
|
||||
stage('Prepare') {
|
||||
stage('Prepare')
|
||||
{
|
||||
// Ensure we start with a clean build directory.
|
||||
step([$class: 'WsCleanup'])
|
||||
|
||||
|
@ -11,37 +14,17 @@ parallel_nodes(['linux && cura', 'windows && cura']) {
|
|||
}
|
||||
|
||||
// If any error occurs during building, we want to catch it and continue with the "finale" stage.
|
||||
catchError {
|
||||
stage('Pre Checks') {
|
||||
if (isUnix()) {
|
||||
// Check shortcut keys
|
||||
try {
|
||||
sh """
|
||||
echo 'Check for duplicate shortcut keys in all translation files.'
|
||||
${env.CURA_ENVIRONMENT_PATH}/master/bin/python3 scripts/check_shortcut_keys.py
|
||||
"""
|
||||
} 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
catchError
|
||||
{
|
||||
// Building and testing should happen in a subdirectory.
|
||||
dir('build') {
|
||||
dir('build')
|
||||
{
|
||||
// Perform the "build". Since Uranium is Python code, this basically only ensures CMake is setup.
|
||||
stage('Build') {
|
||||
stage('Build')
|
||||
{
|
||||
def branch = env.BRANCH_NAME
|
||||
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}")) {
|
||||
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}"))
|
||||
{
|
||||
branch = "master"
|
||||
}
|
||||
|
||||
|
@ -51,11 +34,14 @@ parallel_nodes(['linux && cura', 'windows && cura']) {
|
|||
}
|
||||
|
||||
// Try and run the unit tests. If this stage fails, we consider the build to be "unstable".
|
||||
stage('Unit Test') {
|
||||
if (isUnix()) {
|
||||
stage('Unit Test')
|
||||
{
|
||||
if (isUnix())
|
||||
{
|
||||
// For Linux to show everything
|
||||
def branch = env.BRANCH_NAME
|
||||
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}")) {
|
||||
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}"))
|
||||
{
|
||||
branch = "master"
|
||||
}
|
||||
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
|
||||
|
@ -66,37 +52,48 @@ parallel_nodes(['linux && cura', 'windows && cura']) {
|
|||
export PYTHONPATH=.:"${uranium_dir}"
|
||||
${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/pytest -x --verbose --full-trace --capture=no ./tests
|
||||
"""
|
||||
} catch(e) {
|
||||
} catch(e)
|
||||
{
|
||||
currentBuild.result = "UNSTABLE"
|
||||
}
|
||||
}
|
||||
else {
|
||||
else
|
||||
{
|
||||
// For Windows
|
||||
try {
|
||||
try
|
||||
{
|
||||
// This also does code style checks.
|
||||
bat 'ctest -V'
|
||||
} catch(e) {
|
||||
} catch(e)
|
||||
{
|
||||
currentBuild.result = "UNSTABLE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Code Style') {
|
||||
if (isUnix()) {
|
||||
// For Linux to show everything
|
||||
stage('Code Style')
|
||||
{
|
||||
if (isUnix())
|
||||
{
|
||||
// For Linux to show everything.
|
||||
// CMake also runs this test, but if it fails then the test just shows "failed" without details of what exactly failed.
|
||||
def branch = env.BRANCH_NAME
|
||||
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}")) {
|
||||
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}"))
|
||||
{
|
||||
branch = "master"
|
||||
}
|
||||
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
sh """
|
||||
cd ..
|
||||
export PYTHONPATH=.:"${uranium_dir}"
|
||||
${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/python3 run_mypy.py
|
||||
"""
|
||||
} catch(e) {
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
currentBuild.result = "UNSTABLE"
|
||||
}
|
||||
}
|
||||
|
@ -105,7 +102,8 @@ parallel_nodes(['linux && cura', 'windows && cura']) {
|
|||
}
|
||||
|
||||
// Perform any post-build actions like notification and publishing of unit tests.
|
||||
stage('Finalize') {
|
||||
stage('Finalize')
|
||||
{
|
||||
// Publish the test results to Jenkins.
|
||||
junit allowEmptyResults: true, testResults: 'build/junit*.xml'
|
||||
|
||||
|
|
|
@ -57,5 +57,13 @@ endforeach()
|
|||
#Add code style test.
|
||||
add_test(
|
||||
NAME "code-style"
|
||||
COMMAND ${PYTHON_EXECUTABLE} run_mypy.py WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
COMMAND ${PYTHON_EXECUTABLE} run_mypy.py
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
)
|
||||
|
||||
#Add test for whether the shortcut alt-keys are unique in every translation.
|
||||
add_test(
|
||||
NAME "shortcut-keys"
|
||||
COMMAND ${PYTHON_EXECUTABLE} scripts/check_shortcut_keys.py
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
)
|
|
@ -712,10 +712,8 @@ class CuraApplication(QtApplication):
|
|||
self._print_information = PrintInformation.PrintInformation(self)
|
||||
self._cura_actions = CuraActions.CuraActions(self)
|
||||
|
||||
# Initialize setting visibility presets model
|
||||
self._setting_visibility_presets_model = SettingVisibilityPresetsModel(self)
|
||||
default_visibility_profile = self._setting_visibility_presets_model.getItem(0)
|
||||
self.getPreferences().setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"]))
|
||||
# Initialize setting visibility presets model.
|
||||
self._setting_visibility_presets_model = SettingVisibilityPresetsModel(self.getPreferences(), parent = self)
|
||||
|
||||
# Initialize Cura API
|
||||
self._cura_API.initialize()
|
||||
|
|
|
@ -1,135 +1,109 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Optional, List, Dict, Union
|
||||
import os
|
||||
import urllib.parse
|
||||
from configparser import ConfigParser
|
||||
from typing import Optional, List
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, Qt, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Resources import Resources
|
||||
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from cura.Settings.SettingVisibilityPreset import SettingVisibilityPreset
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class SettingVisibilityPresetsModel(ListModel):
|
||||
IdRole = Qt.UserRole + 1
|
||||
NameRole = Qt.UserRole + 2
|
||||
SettingsRole = Qt.UserRole + 3
|
||||
class SettingVisibilityPresetsModel(QObject):
|
||||
onItemsChanged = pyqtSignal()
|
||||
activePresetChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, parent = None):
|
||||
def __init__(self, preferences, parent = None):
|
||||
super().__init__(parent)
|
||||
self.addRoleName(self.IdRole, "id")
|
||||
self.addRoleName(self.NameRole, "name")
|
||||
self.addRoleName(self.SettingsRole, "settings")
|
||||
|
||||
self._items = [] # type: List[SettingVisibilityPreset]
|
||||
self._populate()
|
||||
basic_item = self.items[1]
|
||||
basic_visibile_settings = ";".join(basic_item["settings"])
|
||||
|
||||
self._preferences = Application.getInstance().getPreferences()
|
||||
basic_item = self.getVisibilityPresetById("basic")
|
||||
basic_visibile_settings = ";".join(basic_item.settings)
|
||||
|
||||
self._preferences = preferences
|
||||
|
||||
# Preference to store which preset is currently selected
|
||||
self._preferences.addPreference("cura/active_setting_visibility_preset", "basic")
|
||||
|
||||
# Preference that stores the "custom" set so it can always be restored (even after a restart)
|
||||
self._preferences.addPreference("cura/custom_visible_settings", basic_visibile_settings)
|
||||
self._preferences.preferenceChanged.connect(self._onPreferencesChanged)
|
||||
|
||||
self._active_preset_item = self._getItem(self._preferences.getValue("cura/active_setting_visibility_preset"))
|
||||
self._active_preset_item = self.getVisibilityPresetById(self._preferences.getValue("cura/active_setting_visibility_preset"))
|
||||
|
||||
# Initialize visible settings if it is not done yet
|
||||
visible_settings = self._preferences.getValue("general/visible_settings")
|
||||
|
||||
if not visible_settings:
|
||||
self._preferences.setValue("general/visible_settings", ";".join(self._active_preset_item["settings"]))
|
||||
self._preferences.setValue("general/visible_settings", ";".join(self._active_preset_item.settings))
|
||||
else:
|
||||
self._onPreferencesChanged("general/visible_settings")
|
||||
|
||||
self.activePresetChanged.emit()
|
||||
|
||||
def _getItem(self, item_id: str) -> Optional[dict]:
|
||||
def getVisibilityPresetById(self, item_id: str) -> Optional[SettingVisibilityPreset]:
|
||||
result = None
|
||||
for item in self.items:
|
||||
if item["id"] == item_id:
|
||||
for item in self._items:
|
||||
if item.presetId == item_id:
|
||||
result = item
|
||||
break
|
||||
return result
|
||||
|
||||
def _populate(self) -> None:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
items = [] # type: List[Dict[str, Union[str, int, List[str]]]]
|
||||
items = [] # type: List[SettingVisibilityPreset]
|
||||
|
||||
custom_preset = SettingVisibilityPreset(preset_id="custom", name ="Custom selection", weight = -100)
|
||||
items.append(custom_preset)
|
||||
for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
|
||||
setting_visibility_preset = SettingVisibilityPreset()
|
||||
try:
|
||||
mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path)
|
||||
except MimeTypeNotFoundError:
|
||||
Logger.log("e", "Could not determine mime type of file %s", file_path)
|
||||
continue
|
||||
|
||||
item_id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(file_path)))
|
||||
if not os.path.isfile(file_path):
|
||||
Logger.log("e", "[%s] is not a file", 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") or not parser.has_option("general", "weight"):
|
||||
continue
|
||||
|
||||
settings = [] # type: List[str]
|
||||
for section in parser.sections():
|
||||
if section == 'general':
|
||||
continue
|
||||
|
||||
settings.append(section)
|
||||
for option in parser[section].keys():
|
||||
settings.append(option)
|
||||
|
||||
items.append({
|
||||
"id": item_id,
|
||||
"name": catalog.i18nc("@action:inmenu", parser["general"]["name"]),
|
||||
"weight": parser["general"]["weight"],
|
||||
"settings": settings,
|
||||
})
|
||||
|
||||
setting_visibility_preset.loadFromFile(file_path)
|
||||
except Exception:
|
||||
Logger.logException("e", "Failed to load setting preset %s", file_path)
|
||||
|
||||
items.sort(key = lambda k: (int(k["weight"]), k["id"])) # type: ignore
|
||||
# Put "custom" at the top
|
||||
items.insert(0, {"id": "custom",
|
||||
"name": "Custom selection",
|
||||
"weight": -100,
|
||||
"settings": []})
|
||||
items.append(setting_visibility_preset)
|
||||
|
||||
# Sort them on weight (and if that fails, use ID)
|
||||
items.sort(key = lambda k: (int(k.weight), k.presetId))
|
||||
|
||||
self.setItems(items)
|
||||
|
||||
@pyqtProperty("QVariantList", notify = onItemsChanged)
|
||||
def items(self):
|
||||
return self._items
|
||||
|
||||
def setItems(self, items: List[SettingVisibilityPreset]) -> None:
|
||||
if self._items != items:
|
||||
self._items = items
|
||||
self.onItemsChanged.emit()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setActivePreset(self, preset_id: str):
|
||||
if preset_id == self._active_preset_item["id"]:
|
||||
def setActivePreset(self, preset_id: str) -> None:
|
||||
if preset_id == self._active_preset_item.presetId:
|
||||
Logger.log("d", "Same setting visibility preset [%s] selected, do nothing.", preset_id)
|
||||
return
|
||||
|
||||
preset_item = None
|
||||
for item in self.items:
|
||||
if item["id"] == preset_id:
|
||||
preset_item = item
|
||||
break
|
||||
preset_item = self.getVisibilityPresetById(preset_id)
|
||||
if preset_item is None:
|
||||
Logger.log("w", "Tried to set active preset to unknown id [%s]", preset_id)
|
||||
return
|
||||
|
||||
need_to_save_to_custom = self._active_preset_item["id"] == "custom" and preset_id != "custom"
|
||||
need_to_save_to_custom = self._active_preset_item.presetId == "custom" and preset_id != "custom"
|
||||
if need_to_save_to_custom:
|
||||
# Save the current visibility settings to custom
|
||||
current_visibility_string = self._preferences.getValue("general/visible_settings")
|
||||
if current_visibility_string:
|
||||
self._preferences.setValue("cura/custom_visible_settings", current_visibility_string)
|
||||
|
||||
new_visibility_string = ";".join(preset_item["settings"])
|
||||
new_visibility_string = ";".join(preset_item.settings)
|
||||
if preset_id == "custom":
|
||||
# Get settings from the stored custom data
|
||||
new_visibility_string = self._preferences.getValue("cura/custom_visible_settings")
|
||||
|
@ -141,11 +115,9 @@ class SettingVisibilityPresetsModel(ListModel):
|
|||
self._active_preset_item = preset_item
|
||||
self.activePresetChanged.emit()
|
||||
|
||||
activePresetChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(str, notify = activePresetChanged)
|
||||
def activePreset(self) -> str:
|
||||
return self._active_preset_item["id"]
|
||||
return self._active_preset_item.presetId
|
||||
|
||||
def _onPreferencesChanged(self, name: str) -> None:
|
||||
if name != "general/visible_settings":
|
||||
|
@ -158,25 +130,26 @@ class SettingVisibilityPresetsModel(ListModel):
|
|||
|
||||
visibility_set = set(visibility_string.split(";"))
|
||||
matching_preset_item = None
|
||||
for item in self.items:
|
||||
if item["id"] == "custom":
|
||||
for item in self._items:
|
||||
if item.presetId == "custom":
|
||||
continue
|
||||
if set(item["settings"]) == visibility_set:
|
||||
if set(item.settings) == visibility_set:
|
||||
matching_preset_item = item
|
||||
break
|
||||
|
||||
item_to_set = self._active_preset_item
|
||||
if matching_preset_item is None:
|
||||
# The new visibility setup is "custom" should be custom
|
||||
if self._active_preset_item["id"] == "custom":
|
||||
if self._active_preset_item.presetId == "custom":
|
||||
# We are already in custom, just save the settings
|
||||
self._preferences.setValue("cura/custom_visible_settings", visibility_string)
|
||||
else:
|
||||
item_to_set = self.items[0] # 0 is custom
|
||||
# We need to move to custom preset.
|
||||
item_to_set = self.getVisibilityPresetById("custom")
|
||||
else:
|
||||
item_to_set = matching_preset_item
|
||||
|
||||
if self._active_preset_item is None or self._active_preset_item["id"] != item_to_set["id"]:
|
||||
if self._active_preset_item is None or self._active_preset_item.presetId != item_to_set.presetId:
|
||||
self._active_preset_item = item_to_set
|
||||
self._preferences.setValue("cura/active_setting_visibility_preset", self._active_preset_item["id"])
|
||||
self._preferences.setValue("cura/active_setting_visibility_preset", self._active_preset_item.presetId)
|
||||
self.activePresetChanged.emit()
|
||||
|
|
90
cura/Settings/SettingVisibilityPreset.py
Normal file
90
cura/Settings/SettingVisibilityPreset.py
Normal file
|
@ -0,0 +1,90 @@
|
|||
import os
|
||||
import urllib.parse
|
||||
from configparser import ConfigParser
|
||||
from typing import List
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.MimeTypeDatabase import MimeTypeDatabase
|
||||
|
||||
|
||||
class SettingVisibilityPreset(QObject):
|
||||
onSettingsChanged = pyqtSignal()
|
||||
onNameChanged = pyqtSignal()
|
||||
onWeightChanged = pyqtSignal()
|
||||
onIdChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, preset_id: str = "", name: str = "", weight: int = 0, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._settings = [] # type: List[str]
|
||||
self._id = preset_id
|
||||
self._weight = weight
|
||||
self._name = name
|
||||
|
||||
@pyqtProperty("QStringList", notify = onSettingsChanged)
|
||||
def settings(self) -> List[str]:
|
||||
return self._settings
|
||||
|
||||
@pyqtProperty(str, notify = onIdChanged)
|
||||
def presetId(self) -> str:
|
||||
return self._id
|
||||
|
||||
@pyqtProperty(int, notify = onWeightChanged)
|
||||
def weight(self) -> int:
|
||||
return self._weight
|
||||
|
||||
@pyqtProperty(str, notify = onNameChanged)
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
def setName(self, name: str) -> None:
|
||||
if name != self._name:
|
||||
self._name = name
|
||||
self.onNameChanged.emit()
|
||||
|
||||
def setId(self, id: str) -> None:
|
||||
if id != self._id:
|
||||
self._id = id
|
||||
self.onIdChanged.emit()
|
||||
|
||||
def setWeight(self, weight: int) -> None:
|
||||
if weight != self._weight:
|
||||
self._weight = weight
|
||||
self.onWeightChanged.emit()
|
||||
|
||||
def setSettings(self, settings: List[str]) -> None:
|
||||
if set(settings) != set(self._settings):
|
||||
self._settings = list(set(settings)) # filter out non unique
|
||||
self.onSettingsChanged.emit()
|
||||
|
||||
# Load a preset from file. We expect a file that can be parsed by means of the config parser.
|
||||
# The sections indicate the categories and the parameters placed in it (which don't need values) are the settings
|
||||
# that should be considered visible.
|
||||
def loadFromFile(self, file_path: str) -> None:
|
||||
mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path)
|
||||
|
||||
item_id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(file_path)))
|
||||
if not os.path.isfile(file_path):
|
||||
Logger.log("e", "[%s] is not a file", file_path)
|
||||
return None
|
||||
|
||||
parser = ConfigParser(interpolation = None, allow_no_value = True) # Accept options without any value,
|
||||
|
||||
parser.read([file_path])
|
||||
if not parser.has_option("general", "name") or not parser.has_option("general", "weight"):
|
||||
return None
|
||||
|
||||
settings = [] # type: List[str]
|
||||
for section in parser.sections():
|
||||
if section == "general":
|
||||
continue
|
||||
|
||||
settings.append(section)
|
||||
for option in parser[section].keys():
|
||||
settings.append(option)
|
||||
self.setSettings(settings)
|
||||
self.setId(item_id)
|
||||
self.setName(parser["general"]["name"])
|
||||
self.setWeight(int(parser["general"]["weight"]))
|
||||
|
|
@ -18,17 +18,17 @@ Menu
|
|||
|
||||
Instantiator
|
||||
{
|
||||
model: settingVisibilityPresetsModel
|
||||
model: settingVisibilityPresetsModel.items
|
||||
|
||||
MenuItem
|
||||
{
|
||||
text: model.name
|
||||
text: modelData.name
|
||||
checkable: true
|
||||
checked: model.id == settingVisibilityPresetsModel.activePreset
|
||||
checked: modelData.presetId == settingVisibilityPresetsModel.activePreset
|
||||
exclusiveGroup: group
|
||||
onTriggered:
|
||||
{
|
||||
settingVisibilityPresetsModel.setActivePreset(model.id);
|
||||
settingVisibilityPresetsModel.setActivePreset(modelData.presetId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -110,24 +110,25 @@ UM.PreferencesPage
|
|||
right: parent.right
|
||||
}
|
||||
|
||||
model: settingVisibilityPresetsModel
|
||||
model: settingVisibilityPresetsModel.items
|
||||
textRole: "name"
|
||||
|
||||
currentIndex:
|
||||
{
|
||||
// Load previously selected preset.
|
||||
var index = settingVisibilityPresetsModel.find("id", settingVisibilityPresetsModel.activePreset)
|
||||
if (index == -1)
|
||||
for(var i = 0; i < settingVisibilityPresetsModel.items.length; ++i)
|
||||
{
|
||||
return 0
|
||||
if(settingVisibilityPresetsModel.items[i].id == settingVisibilityPresetsModel.activePreset)
|
||||
{
|
||||
currentIndex = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return index
|
||||
return -1
|
||||
}
|
||||
|
||||
onActivated:
|
||||
{
|
||||
var preset_id = settingVisibilityPresetsModel.getItem(index).id;
|
||||
var preset_id = settingVisibilityPresetsModel.items[index].id;
|
||||
settingVisibilityPresetsModel.setActivePreset(preset_id);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,239 +0,0 @@
|
|||
#!/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:
|
||||
# <visibility_name> -> <category> -> <list-fo-setting-keys>
|
||||
# "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:
|
||||
# <visibility_name> -> 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()
|
89
tests/Settings/TestSettingVisibilityPresets.py
Normal file
89
tests/Settings/TestSettingVisibilityPresets.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
from unittest.mock import MagicMock
|
||||
|
||||
import os.path
|
||||
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Resources import Resources
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel
|
||||
from cura.Settings.SettingVisibilityPreset import SettingVisibilityPreset
|
||||
|
||||
setting_visibility_preset_test_settings = {"test", "zomg", "derp", "yay", "whoo"}
|
||||
|
||||
Resources.addSearchPath(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "resources")))
|
||||
Resources.addStorageType(CuraApplication.ResourceTypes.SettingVisibilityPreset, "setting_visibility")
|
||||
|
||||
|
||||
def test_createVisibilityPresetFromLocalFile():
|
||||
# Simple creation test. This is seperated from the visibilityFromPrevious, since we can't check for the contents
|
||||
# of the other profiles, since they might change over time.
|
||||
visibility_preset = SettingVisibilityPreset()
|
||||
|
||||
visibility_preset.loadFromFile(os.path.join(os.path.dirname(os.path.abspath(__file__)), "setting_visiblity_preset_test.cfg"))
|
||||
assert setting_visibility_preset_test_settings == set(visibility_preset.settings)
|
||||
|
||||
assert visibility_preset.name == "test"
|
||||
assert visibility_preset.weight == 1
|
||||
assert visibility_preset.settings.count("yay") == 1 # It's in the file twice but we should load it once.
|
||||
|
||||
def test_visibilityFromPrevious():
|
||||
# This test checks that all settings in basic are in advanced and all settings in advanced are in expert.
|
||||
|
||||
visibility_model = SettingVisibilityPresetsModel(Preferences())
|
||||
|
||||
basic_visibility = visibility_model.getVisibilityPresetById("basic")
|
||||
advanced_visibility = visibility_model.getVisibilityPresetById("advanced")
|
||||
expert_visibility = visibility_model.getVisibilityPresetById("expert")
|
||||
|
||||
# Check if there are settings that are in basic, but not in advanced.
|
||||
settings_not_in_advanced = set(basic_visibility.settings) - set(advanced_visibility.settings)
|
||||
assert len(settings_not_in_advanced) == 0 # All settings in basic should be in advanced
|
||||
|
||||
# Check if there are settings that are in advanced, but not in expert.
|
||||
settings_not_in_expert = set(advanced_visibility.settings) - set(expert_visibility.settings)
|
||||
assert len(settings_not_in_expert) == 0 # All settings in advanced should be in expert.
|
||||
|
||||
|
||||
def test_setActivePreset():
|
||||
preferences = Preferences()
|
||||
visibility_model = SettingVisibilityPresetsModel(preferences)
|
||||
visibility_model.activePresetChanged = MagicMock()
|
||||
# Ensure that we start off with basic (since we didn't change anyting just yet!)
|
||||
assert visibility_model.activePreset == "basic"
|
||||
|
||||
# Everything should be the same.
|
||||
visibility_model.setActivePreset("basic")
|
||||
assert visibility_model.activePreset == "basic"
|
||||
assert visibility_model.activePresetChanged.emit.call_count == 0 # No events should be sent.
|
||||
|
||||
# Change it to existing type (should work...)
|
||||
visibility_model.setActivePreset("advanced")
|
||||
assert visibility_model.activePreset == "advanced"
|
||||
assert visibility_model.activePresetChanged.emit.call_count == 1
|
||||
|
||||
# Change to unknown preset. Shouldn't do anything.
|
||||
visibility_model.setActivePreset("OMGZOMGNOPE")
|
||||
assert visibility_model.activePreset == "advanced"
|
||||
assert visibility_model.activePresetChanged.emit.call_count == 1
|
||||
|
||||
|
||||
def test_preferenceChanged():
|
||||
preferences = Preferences()
|
||||
# Set the visible_settings to something silly
|
||||
preferences.addPreference("general/visible_settings", "omgzomg")
|
||||
visibility_model = SettingVisibilityPresetsModel(preferences)
|
||||
visibility_model.activePresetChanged = MagicMock()
|
||||
|
||||
assert visibility_model.activePreset == "custom" # This should make the model start at "custom
|
||||
assert visibility_model.activePresetChanged.emit.call_count == 0
|
||||
|
||||
|
||||
basic_visibility = visibility_model.getVisibilityPresetById("basic")
|
||||
new_visibility_string = ";".join(basic_visibility.settings)
|
||||
preferences.setValue("general/visible_settings", new_visibility_string)
|
||||
|
||||
# Fake a signal emit (since we didn't create the application, our own signals are not fired)
|
||||
visibility_model._onPreferencesChanged("general/visible_settings")
|
||||
# Set the visibility settings to basic
|
||||
assert visibility_model.activePreset == "basic"
|
||||
assert visibility_model.activePresetChanged.emit.call_count == 1
|
11
tests/Settings/setting_visiblity_preset_test.cfg
Normal file
11
tests/Settings/setting_visiblity_preset_test.cfg
Normal file
|
@ -0,0 +1,11 @@
|
|||
[general]
|
||||
name = test
|
||||
weight = 1
|
||||
|
||||
[test]
|
||||
zomg
|
||||
derp
|
||||
yay
|
||||
|
||||
[whoo]
|
||||
yay
|
Loading…
Add table
Add a link
Reference in a new issue