def _buildReplacementTokens(self, stack: ContainerStack) -> Dict[str, Any]: """Creates a dictionary of tokens to replace in g-code pieces. This indicates what should be replaced in the start and end g-codes. :param stack: The stack to get the settings from to replace the tokens with. :return: A dictionary of replacement tokens to the values they should be replaced with. """ result = {} for key in stack.getAllKeys(): result[key] = stack.getProperty(key, "value") Job.yieldThread() # Material identification in addition to non-human-readable GUID result["material_id"] = stack.material.getMetaDataEntry( "base_file", "") result["material_type"] = stack.material.getMetaDataEntry( "material", "") result["material_name"] = stack.material.getMetaDataEntry("name", "") result["material_brand"] = stack.material.getMetaDataEntry("brand", "") # Renamed settings. result["print_bed_temperature"] = result["material_bed_temperature"] result["print_temperature"] = result["material_print_temperature"] result["travel_speed"] = result["speed_travel"] #Some extra settings. result["time"] = time.strftime("%H:%M:%S") result["date"] = time.strftime("%d-%m-%Y") result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))] result["initial_extruder_nr"] = CuraApplication.getInstance( ).getExtruderManager().getInitialExtruderNr() return result
def _buildReplacementTokens(self, stack: ContainerStack) -> Dict[str, Any]: result = {} for key in stack.getAllKeys(): value = stack.getProperty(key, "value") result[key] = value Job.yieldThread() result["print_bed_temperature"] = result[ "material_bed_temperature"] # Renamed settings. result["print_bed_temperature_layer_0"] = result[ "material_bed_temperature_layer_0"] # Renamed settings. result["print_temperature"] = result["material_print_temperature"] result["print_chamber_temperature"] = result[ "default_material_chamber_temperature"] result["travel_speed"] = result["speed_travel"] result["time"] = time.strftime("%H:%M:%S") #Some extra settings. result["date"] = time.strftime("%d-%m-%Y") result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))] if (result["support_solubile"] == True): print("SUPPORT TRUE!!!!") result["support_bottom_distance"] = "0.0" result["support_top_distance"] = "0.0" initial_extruder_stack = CuraApplication.getInstance( ).getExtruderManager().getUsedExtruderStacks()[0] initial_extruder_nr = initial_extruder_stack.getProperty( "extruder_nr", "value") result["initial_extruder_nr"] = initial_extruder_nr return result
def _buildReplacementTokens(self, stack: ContainerStack) -> Dict[str, Any]: result = {} for key in stack.getAllKeys(): value = stack.getProperty(key, "value") result[key] = value Job.yieldThread() result["print_bed_temperature"] = result[ "material_bed_temperature"] # Renamed settings. result["print_temperature"] = result["material_print_temperature"] result["time"] = time.strftime("%H:%M:%S") # Some extra settings. result["date"] = time.strftime("%d-%m-%Y") result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))] printing_mode = result["printing_mode"] if printing_mode in ["cylindrical_full", "cylindrical"]: result["cylindrical_rotate"] = "G0 A%.2f" % ( 90 * result["machine_a_axis_multiplier"] / result["machine_a_axis_divider"]) result["coordinate_system"] = "G56" elif printing_mode in ["spherical_full", "spherical"]: result["cylindrical_rotate"] = "G0 A0" result["coordinate_system"] = "G55" initial_extruder_stack = SteSlicerApplication.getInstance( ).getExtruderManager().getUsedExtruderStacks()[0] initial_extruder_nr = initial_extruder_stack.getProperty( "extruder_nr", "value") result["initial_extruder_nr"] = initial_extruder_nr return result
def _buildGlobalInheritsStackMessage(self, stack: ContainerStack) -> None: for key in stack.getAllKeys(): extruder_position = int(round(float(stack.getProperty(key, "limit_to_extruder")))) if extruder_position >= 0: # Set to a specific extruder. setting_extruder = self._slice_message.addRepeatedMessage("limit_to_extruder") setting_extruder.name = key setting_extruder.extruder = extruder_position Job.yieldThread()
def _checkStackForErrors(self, stack: ContainerStack) -> bool: if stack is None: return False for key in stack.getAllKeys(): validation_state = stack.getProperty(key, "validationState") if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError): Logger.log("w", "Setting %s is not valid, but %s. Aborting slicing.", key, validation_state) return True Job.yieldThread() return False
def _checkStackForErrors(self, stack: ContainerStack) -> bool: if stack is None: return False for key in stack.getAllKeys(): validation_state = stack.getProperty(key, "validationState") if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError, ValidatorState.Invalid): Logger.log("w", "Setting %s is not valid, but %s. Aborting slicing.", key, validation_state) return True Job.yieldThread() return False
def _buildReplacementTokens(self, stack: ContainerStack) -> Dict[str, Any]: result = {} for key in stack.getAllKeys(): value = stack.getProperty(key, "value") result[key] = value Job.yieldThread() result["print_bed_temperature"] = result["material_bed_temperature"] # Renamed settings. result["print_temperature"] = result["material_print_temperature"] result["travel_speed"] = result["speed_travel"] result["time"] = time.strftime("%H:%M:%S") #Some extra settings. result["date"] = time.strftime("%d-%m-%Y") result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))] result["initial_extruder_nr"] = CuraApplication.getInstance().getExtruderManager().getInitialExtruderNr() return result
def _checkStackForErrors(self, stack: ContainerStack) -> bool: if stack is None: return False # if there are no per-object settings we don't need to check the other settings here stack_top = stack.getTop() if stack_top is None or not stack_top.getAllKeys(): return False for key in stack.getAllKeys(): validation_state = stack.getProperty(key, "validationState") if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError, ValidatorState.Invalid): Logger.log("w", "Setting %s is not valid, but %s. Aborting slicing.", key, validation_state) return True Job.yieldThread() return False
def _buildGlobalInheritsStackMessage(self, stack: ContainerStack) -> None: """Sends for some settings which extruder they should fallback to if not set. This is only set for settings that have the limit_to_extruder property. :param stack: The global stack with all settings, from which to read the limit_to_extruder property. """ for key in stack.getAllKeys(): extruder_position = int(round(float(stack.getProperty(key, "limit_to_extruder")))) if extruder_position >= 0: # Set to a specific extruder. setting_extruder = self._slice_message.addRepeatedMessage("limit_to_extruder") setting_extruder.name = key setting_extruder.extruder = extruder_position Job.yieldThread()
def _buildReplacementTokens(self, stack: ContainerStack) -> Dict[str, Any]: result = {} for key in stack.getAllKeys(): value = stack.getProperty(key, "value") result[key] = value Job.yieldThread() result["print_bed_temperature"] = result["material_bed_temperature"] # Renamed settings. result["print_temperature"] = result["material_print_temperature"] result["travel_speed"] = result["speed_travel"] result["time"] = time.strftime("%H:%M:%S") #Some extra settings. result["date"] = time.strftime("%d-%m-%Y") result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))] initial_extruder_stack = CuraApplication.getInstance().getExtruderManager().getUsedExtruderStacks()[0] initial_extruder_nr = initial_extruder_stack.getProperty("extruder_nr", "value") result["initial_extruder_nr"] = initial_extruder_nr return result
class CuraSettingsGuide(Extension, QObject): """ The main manager and entry point for the Cura Settings Guide. This adds a menu item to the extensions menu and to the context menu of every article. It creates a dialogue window to display the guide in, and makes the menu items open said dialogue window. This class is also exposed to the QML code, which can use it to request the articles and metadata about the article. """ def __init__(self, parent=None) -> None: """ Executed during registration of the plug-in. This adds a menu item to the extensions menu and the context menu of the settings. :param parent: The parent QObject this is located in. Unused by this particular object. """ QObject.__init__(self, parent) Extension.__init__(self) self.setMenuName("Ѕettings Guide") # Using Cyrillic Ѕ instead of normal S to prevent MacOS detecting the word "setting" and pulling this menu item out of context. self.addMenuItem("Ѕettings Guide", self.startWelcomeGuide) self._dialog = None # Cached instance of the dialogue window. self._container_stack = None # Stack that provides not only the normal settings but also the extra articles added by this guide. self._markdown_per_folder = {} # For each directory containing Markdown files, create one renderer that correctly dereferences images relatively. self.articles = {} # type: Dict[str, Dict[str, List[List[str]]]] # All of the articles by key and language. Key: article ID, value: Dict with language as key and lists of items in each article as value. self.load_definitions() self.article_locations = self.find_articles() self._selected_article_id = "" # Which article is currently shown for the user. Empty string indicates it's the welcome screen. # Add context menu item to the settings list to open the guide for that setting. application = CuraApplication.getInstance() self._selected_language = application.getPreferences().getValue("general/language") application.getCuraAPI().interface.settings.addContextMenuItem({ "name": "Settings Guide", "icon_name": "help-contents", "actions": ["sidebarMenuItemOnClickHandler"], "menu_item": MenuItemHandler.MenuItemHandler(self) }) application.getPreferences().addPreference("settings_guide/show+articles+in+setting+tooltips+%28requires+restart%29", False) self.adjust_theme() application.initializationFinished.connect(self.load_all_in_background) def adjust_theme(self): """ Makes the tooltips wider, if displaying articles in the tooltips. """ application = CuraApplication.getInstance() preferences = application.getPreferences() if preferences.getValue("settings_guide/show+articles+in+setting+tooltips+%28requires+restart%29"): preferences.addPreference("general/theme", application.default_theme) theme_name = preferences.getValue("general/theme") if theme_name.endswith("_settingsguideadjust"): return # Already adjusted. theme_path = Resources.getPath(Resources.Themes, theme_name) # Create a new theme where we can adjust the tooltip. new_theme_name = theme_name + "_settingsguideadjust" new_theme_path = Resources.getStoragePath(Resources.Themes, new_theme_name) try: shutil.copytree(theme_path, new_theme_path) except OSError: # Already exists. Happens when the user manually adjusted the theme back in the preferences screen. try: os.removedirs(new_theme_path) shutil.copytree(theme_path, new_theme_path) except OSError: pass # Perhaps no rights? # Adjust the tooltip width. with open(os.path.join(theme_path, "theme.json")) as f: adjusted_theme = json.load(f) if "sizes" not in adjusted_theme: adjusted_theme["sizes"] = {} adjusted_theme["sizes"]["tooltip"] = [50.0, 10.0] with open(os.path.join(new_theme_path, "theme.json"), "w") as f: json.dump(adjusted_theme, f) # Enable the new theme. preferences.setValue("general/theme", new_theme_name) application.default_theme = new_theme_name def load_all_in_background(self): """ Runs the load_all() function as a background task. """ class ArticleLoadJob(Job): """ A background task that loads all articles of the guide. """ def __init__(self, guide): """ Creates the background task. :param guide: The CuraSettingsGuide object which has the function to call. """ self.guide = guide def run(self): """ Runs the background task. Cura will call this function from a different thread. """ self.guide.load_all() JobQueue.getInstance().add(ArticleLoadJob(self)) def load_all(self): """ Pre-cache all articles. This is meant to run as a background task. This makes sure all setting descriptions are loaded so that they can load faster next time the article is requested. It also makes sure that the setting description is correctly displayed (after a while). """ for article_id in self._container_stack.getAllKeys(): self._getArticle(article_id) # Load articles one by one. Logger.log("i", "Finished loading Settings Guide articles.") def load_definitions(self): """ Load all the setting definitions into a custom container stack. This container stack also contains extra entries for the articles that are not settings. The result is stored in self._container_stack. """ if self._container_stack: return # Already done earlier. Don't re-load. with open(os.path.join(os.path.dirname(__file__), "resources", "settings_guide_definitions.def.json")) as f: definitions_serialised = f.read() definition_container = DefinitionContainer("settings_guide_definitions") definition_container.deserialize(definitions_serialised) ContainerRegistry.getInstance().addContainer(definition_container) self._container_stack = ContainerStack("settings_guide_stack") self._container_stack.addContainer(definition_container) ContainerRegistry.getInstance().addContainer(self._container_stack) def find_articles(self): """ For each article and language, find where the Markdown file is located. :return: A nested dictionary mapping article ID to language to file path. """ result = {} # Find the English translations first. for root, _, files in os.walk(os.path.join(os.path.dirname(__file__), "resources", "articles")): for filename in files: base_name, extension = os.path.splitext(filename) if extension != ".md": continue # Only interested in article files. result[base_name] = {"en_US": os.path.join(root, filename)} # Find the translated articles in the translations folder. for language in os.listdir(os.path.join(os.path.dirname(__file__), "resources", "translations")): language_path = os.path.join(os.path.dirname(__file__), "resources", "translations", language) if not os.path.isdir(language_path): continue # Not a translation folder. for root, _, files in os.walk(language_path): for filename in files: base_name, extension = os.path.splitext(filename) if extension != ".md": continue # Only interested in article files. if base_name not in result: continue # Translation for an article that doesn't exist in English? result[base_name][language] = os.path.join(root, filename) return result def load_window(self): """ Create the GUI window for the guide. The window's QML element is stored in a field. There can only be one instance at a time. """ self.load_definitions() if self._dialog: return # Dialogue already open. Logger.log("d", "Creating Settings Guide window.") plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId()) if plugin_path is None: plugin_path = os.path.dirname(__file__) path = os.path.join(plugin_path, "resources", "qml", "SettingsGuide.qml") self._dialog = CuraApplication.getInstance().createQmlComponent(path, {"manager": self}) if not self._dialog: Logger.log("e", "Unable to create settings guide dialogue.") def startWelcomeGuide(self) -> None: """ Opens the guide in the welcome page. """ self.load_window() if not self._dialog: return self.setSelectedArticleId("") # Display welcome page. self._dialog.show() def startWelcomeGuideAndSelectArticle(self, article_key: str) -> None: """ Opens the guide and immediately selects a certain article. :param article_key: The key of the article to show the guide of. """ self.load_window() if not self._dialog: return self.currentArticleReset.emit() self.setSelectedArticleId(article_key) self._dialog.show() currentArticleReset = pyqtSignal() #Signal to indicate that the article list should reset its current index. def _getArticle(self, article_id, language="en_US") -> List[List[str]]: """ Gets the rich text of a specified article. This function lazily loads an article from a file. If it's never been loaded before the article gets parsed and stored. Otherwise it'll get taken from the cache. :param article_id: The ID of the article to get. :param language: The language to get the article in. """ if article_id in self.articles and language in self.articles[article_id]: return self.articles[article_id][language] images_path = os.path.join(os.path.dirname(__file__), "resources", "articles", "images") try: if language not in self.article_locations[article_id]: language = "en_US" # Fall back to English if the preferred language is not available. markdown_file = self.article_locations[article_id][language] with open(markdown_file, encoding="utf-8") as f: markdown_str = f.read() images_path = os.path.dirname(markdown_file) except (OSError, KeyError): # File doesn't exist or is otherwise not readable. if self._container_stack and article_id in self._container_stack.getAllKeys(): markdown_str = "*" + self._container_stack.getProperty(article_id, "description") + "*" # Use the setting description as fallback. else: markdown_str = "There is no article on this topic." if images_path not in self._markdown_per_folder: renderer = QtMarkdownRenderer.QtMarkdownRenderer(images_path) self._markdown_per_folder[images_path] = mistune.Markdown(renderer=renderer) # Renders the Markdown articles into the subset of HTML supported by Qt. preferences = CuraApplication.getInstance().getPreferences() find_images = re.compile(r"!\[(.*)\]\(([^\)]+)\)") find_checkboxes = re.compile(r"\[ \]\s*([^\n]+)") image_description = None parts = [] # type: List[List[str]] # List of items in the article. Each item starts with a type ID, and then a variable number of data items. for index, part_between_images in enumerate(find_images.split(markdown_str)): # The parts of the regex split alternate between text, image description and image URL. if index % 3 == 0: part_between_images = part_between_images.strip() if part_between_images or index == 0: parts_between_checkboxes = find_checkboxes.split(part_between_images) for index2, part_between_checkboxes in enumerate(parts_between_checkboxes): part_between_checkboxes = part_between_checkboxes.strip() # The parts of the regex split alternate between text and checkbox description. if index2 % 2 == 0: if part_between_checkboxes: rich_text = self._markdown_per_folder[images_path](part_between_checkboxes) parts.append(["rich_text", rich_text]) else: # if index2 == 1: preference_key = "settings_guide/" + urllib.parse.quote_plus(part_between_checkboxes).lower() parts.append(["checkbox", preference_key, part_between_checkboxes]) elif index % 3 == 1: image_description = mistune.markdown(part_between_images) else: # if index % 3 == 2: if image_description is not None: if parts[-1][0] != "images": # List of images. parts.append(["images"]) image_url = os.path.join(images_path, part_between_images) parts[-1].append(QUrl.fromLocalFile(image_url).toString() + "|" + image_description) image_description = None if article_id not in self.articles: self.articles[article_id] = {} self.articles[article_id][language] = parts if preferences.getValue("settings_guide/show+articles+in+setting+tooltips+%28requires+restart%29"): # Load the article into the actual setting description as well. global_stack = CuraApplication.getInstance().getGlobalContainerStack() if global_stack and article_id in global_stack.getAllKeys(): complete_article = self._markdown_per_folder[images_path](markdown_str) definition = global_stack.definition.findDefinitions(key=article_id)[0] definition._SettingDefinition__property_values["description"] = complete_article return self.articles[article_id][language] @pyqtSlot(str, result=bool) def isArticleFile(self, filename: str) -> bool: """ Tests whether a file name is the file name of an existing article. This test is used to determine if a link should refer to a different article or to the internet. :param filename: The file name to test for. :return: True if the file name is the file name of an existing article, or False if it isn't. """ return os.path.exists(filename) @pyqtProperty(QObject, constant=True) def containerStack(self) -> Optional[ContainerStack]: """ The specialised container stack containing setting definitions for all of the articles in the guide. :return: A container stack with extra definitions for all articles in the guide. """ return self._container_stack @pyqtProperty(str, constant=True) def pluginVersion(self) -> str: """ Get the version number of this plug-in. This plug-in version is a semantic version number in three parts ("1.2.3"). :return: The version number of this plug-in. """ return self.getVersion() selectedArticleChanged = pyqtSignal() @pyqtSlot(str) def setSelectedArticleId(self, article_key: str) -> None: """ Show a certain article in the dialogue. :param article_key: The key of the article to display. """ if self._selected_article_id != article_key: self._selected_article_id = article_key self.selectedArticleChanged.emit() @pyqtProperty(str, fset=setSelectedArticleId, notify=selectedArticleChanged) def selectedArticleId(self) -> str: """ Returns the key of the article that is currently selected. If no article has been selected (the logo is being shown), an empty string is returned. :return: The key of the article that is currently selected. """ return self._selected_article_id @pyqtProperty("QVariantList", notify=selectedArticleChanged) def selectedArticle(self) -> List[List[str]]: """ Returns the currently selected article. This article data is a rich text document, properly formatted from the Markdown files in the article directory. Each article is a list of items, some of which are text and some of which are image lists. :return: The the currently selected article. """ return self._getArticle(self._selected_article_id, self._selected_language)