mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-06 14:37:29 -06:00
Convert doxygen to rst for POS, MonitorStage, PostProcessing
This commit is contained in:
parent
553b09b6cf
commit
a4fe3d7685
9 changed files with 180 additions and 141 deletions
|
@ -1,72 +1,72 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import os.path
|
||||
from UM.Application import Application
|
||||
from cura.Stages.CuraStage import CuraStage
|
||||
|
||||
|
||||
## Stage for monitoring a 3D printing while it's printing.
|
||||
class MonitorStage(CuraStage):
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
# Wait until QML engine is created, otherwise creating the new QML components will fail
|
||||
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
||||
self._printer_output_device = None
|
||||
|
||||
self._active_print_job = None
|
||||
self._active_printer = None
|
||||
|
||||
def _setActivePrintJob(self, print_job):
|
||||
if self._active_print_job != print_job:
|
||||
self._active_print_job = print_job
|
||||
|
||||
def _setActivePrinter(self, printer):
|
||||
if self._active_printer != printer:
|
||||
if self._active_printer:
|
||||
self._active_printer.activePrintJobChanged.disconnect(self._onActivePrintJobChanged)
|
||||
self._active_printer = printer
|
||||
if self._active_printer:
|
||||
self._setActivePrintJob(self._active_printer.activePrintJob)
|
||||
# Jobs might change, so we need to listen to it's changes.
|
||||
self._active_printer.activePrintJobChanged.connect(self._onActivePrintJobChanged)
|
||||
else:
|
||||
self._setActivePrintJob(None)
|
||||
|
||||
def _onActivePrintJobChanged(self):
|
||||
self._setActivePrintJob(self._active_printer.activePrintJob)
|
||||
|
||||
def _onActivePrinterChanged(self):
|
||||
self._setActivePrinter(self._printer_output_device.activePrinter)
|
||||
|
||||
def _onOutputDevicesChanged(self):
|
||||
try:
|
||||
# We assume that you are monitoring the device with the highest priority.
|
||||
new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
|
||||
if new_output_device != self._printer_output_device:
|
||||
if self._printer_output_device:
|
||||
try:
|
||||
self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged)
|
||||
except TypeError:
|
||||
# Ignore stupid "Not connected" errors.
|
||||
pass
|
||||
|
||||
self._printer_output_device = new_output_device
|
||||
|
||||
self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged)
|
||||
self._setActivePrinter(self._printer_output_device.activePrinter)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
def _onEngineCreated(self):
|
||||
# We can only connect now, as we need to be sure that everything is loaded (plugins get created quite early)
|
||||
Application.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
|
||||
self._onOutputDevicesChanged()
|
||||
|
||||
plugin_path = Application.getInstance().getPluginRegistry().getPluginPath(self.getPluginId())
|
||||
if plugin_path is not None:
|
||||
menu_component_path = os.path.join(plugin_path, "MonitorMenu.qml")
|
||||
main_component_path = os.path.join(plugin_path, "MonitorMain.qml")
|
||||
self.addDisplayComponent("menu", menu_component_path)
|
||||
self.addDisplayComponent("main", main_component_path)
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import os.path
|
||||
from UM.Application import Application
|
||||
from cura.Stages.CuraStage import CuraStage
|
||||
|
||||
|
||||
class MonitorStage(CuraStage):
|
||||
"""Stage for monitoring a 3D printing while it's printing."""
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
# Wait until QML engine is created, otherwise creating the new QML components will fail
|
||||
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
||||
self._printer_output_device = None
|
||||
|
||||
self._active_print_job = None
|
||||
self._active_printer = None
|
||||
|
||||
def _setActivePrintJob(self, print_job):
|
||||
if self._active_print_job != print_job:
|
||||
self._active_print_job = print_job
|
||||
|
||||
def _setActivePrinter(self, printer):
|
||||
if self._active_printer != printer:
|
||||
if self._active_printer:
|
||||
self._active_printer.activePrintJobChanged.disconnect(self._onActivePrintJobChanged)
|
||||
self._active_printer = printer
|
||||
if self._active_printer:
|
||||
self._setActivePrintJob(self._active_printer.activePrintJob)
|
||||
# Jobs might change, so we need to listen to it's changes.
|
||||
self._active_printer.activePrintJobChanged.connect(self._onActivePrintJobChanged)
|
||||
else:
|
||||
self._setActivePrintJob(None)
|
||||
|
||||
def _onActivePrintJobChanged(self):
|
||||
self._setActivePrintJob(self._active_printer.activePrintJob)
|
||||
|
||||
def _onActivePrinterChanged(self):
|
||||
self._setActivePrinter(self._printer_output_device.activePrinter)
|
||||
|
||||
def _onOutputDevicesChanged(self):
|
||||
try:
|
||||
# We assume that you are monitoring the device with the highest priority.
|
||||
new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
|
||||
if new_output_device != self._printer_output_device:
|
||||
if self._printer_output_device:
|
||||
try:
|
||||
self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged)
|
||||
except TypeError:
|
||||
# Ignore stupid "Not connected" errors.
|
||||
pass
|
||||
|
||||
self._printer_output_device = new_output_device
|
||||
|
||||
self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged)
|
||||
self._setActivePrinter(self._printer_output_device.activePrinter)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
def _onEngineCreated(self):
|
||||
# We can only connect now, as we need to be sure that everything is loaded (plugins get created quite early)
|
||||
Application.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
|
||||
self._onOutputDevicesChanged()
|
||||
|
||||
plugin_path = Application.getInstance().getPluginRegistry().getPluginPath(self.getPluginId())
|
||||
if plugin_path is not None:
|
||||
menu_component_path = os.path.join(plugin_path, "MonitorMenu.qml")
|
||||
main_component_path = os.path.join(plugin_path, "MonitorMain.qml")
|
||||
self.addDisplayComponent("menu", menu_component_path)
|
||||
self.addDisplayComponent("main", main_component_path)
|
||||
|
|
|
@ -15,9 +15,11 @@ from cura.Settings.ExtruderManager import ExtruderManager #To get global-inherit
|
|||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||
|
||||
|
||||
## The per object setting visibility handler ensures that only setting
|
||||
# definitions that have a matching instance Container are returned as visible.
|
||||
class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHandler.SettingVisibilityHandler):
|
||||
"""The per object setting visibility handler ensures that only setting
|
||||
|
||||
definitions that have a matching instance Container are returned as visible.
|
||||
"""
|
||||
def __init__(self, parent = None, *args, **kwargs):
|
||||
super().__init__(parent = parent, *args, **kwargs)
|
||||
|
||||
|
|
|
@ -12,9 +12,11 @@ from UM.Settings.SettingInstance import SettingInstance
|
|||
from UM.Event import Event
|
||||
|
||||
|
||||
## This tool allows the user to add & change settings per node in the scene.
|
||||
# The settings per object are kept in a ContainerStack, which is linked to a node by decorator.
|
||||
class PerObjectSettingsTool(Tool):
|
||||
"""This tool allows the user to add & change settings per node in the scene.
|
||||
|
||||
The settings per object are kept in a ContainerStack, which is linked to a node by decorator.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._model = None
|
||||
|
@ -48,26 +50,31 @@ class PerObjectSettingsTool(Tool):
|
|||
except AttributeError:
|
||||
return ""
|
||||
|
||||
## Gets the active extruder of the currently selected object.
|
||||
#
|
||||
# \return The active extruder of the currently selected object.
|
||||
def getSelectedActiveExtruder(self):
|
||||
"""Gets the active extruder of the currently selected object.
|
||||
|
||||
:return: The active extruder of the currently selected object.
|
||||
"""
|
||||
|
||||
selected_object = Selection.getSelectedObject(0)
|
||||
return selected_object.callDecoration("getActiveExtruder")
|
||||
|
||||
## Changes the active extruder of the currently selected object.
|
||||
#
|
||||
# \param extruder_stack_id The ID of the extruder to print the currently
|
||||
# selected object with.
|
||||
def setSelectedActiveExtruder(self, extruder_stack_id):
|
||||
"""Changes the active extruder of the currently selected object.
|
||||
|
||||
:param extruder_stack_id: The ID of the extruder to print the currently
|
||||
selected object with.
|
||||
"""
|
||||
|
||||
selected_object = Selection.getSelectedObject(0)
|
||||
stack = selected_object.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
|
||||
if not stack:
|
||||
selected_object.addDecorator(SettingOverrideDecorator())
|
||||
selected_object.callDecoration("setActiveExtruder", extruder_stack_id)
|
||||
|
||||
## Returns True when the mesh_type was changed, False when current mesh_type == mesh_type
|
||||
def setMeshType(self, mesh_type: str) -> bool:
|
||||
"""Returns True when the mesh_type was changed, False when current mesh_type == mesh_type"""
|
||||
|
||||
old_mesh_type = self.getMeshType()
|
||||
if old_mesh_type == mesh_type:
|
||||
return False
|
||||
|
|
|
@ -27,9 +27,8 @@ if TYPE_CHECKING:
|
|||
from .Script import Script
|
||||
|
||||
|
||||
## The post processing plugin is an Extension type plugin that enables pre-written scripts to post process generated
|
||||
# g-code files.
|
||||
class PostProcessingPlugin(QObject, Extension):
|
||||
"""Extension type plugin that enables pre-written scripts to post process g-code files."""
|
||||
def __init__(self, parent = None) -> None:
|
||||
QObject.__init__(self, parent)
|
||||
Extension.__init__(self)
|
||||
|
@ -69,8 +68,9 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
except IndexError:
|
||||
return ""
|
||||
|
||||
## Execute all post-processing scripts on the gcode.
|
||||
def execute(self, output_device) -> None:
|
||||
"""Execute all post-processing scripts on the gcode."""
|
||||
|
||||
scene = Application.getInstance().getController().getScene()
|
||||
# If the scene does not have a gcode, do nothing
|
||||
if not hasattr(scene, "gcode_dict"):
|
||||
|
@ -119,9 +119,10 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
self.selectedIndexChanged.emit() #Ensure that settings are updated
|
||||
self._propertyChanged()
|
||||
|
||||
## Remove a script from the active script list by index.
|
||||
@pyqtSlot(int)
|
||||
def removeScriptByIndex(self, index: int) -> None:
|
||||
"""Remove a script from the active script list by index."""
|
||||
|
||||
self._script_list.pop(index)
|
||||
if len(self._script_list) - 1 < self._selected_script_index:
|
||||
self._selected_script_index = len(self._script_list) - 1
|
||||
|
@ -129,10 +130,12 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
self.selectedIndexChanged.emit() # Ensure that settings are updated
|
||||
self._propertyChanged()
|
||||
|
||||
## Load all scripts from all paths where scripts can be found.
|
||||
#
|
||||
# This should probably only be done on init.
|
||||
def loadAllScripts(self) -> None:
|
||||
"""Load all scripts from all paths where scripts can be found.
|
||||
|
||||
This should probably only be done on init.
|
||||
"""
|
||||
|
||||
if self._loaded_scripts: # Already loaded.
|
||||
return
|
||||
|
||||
|
@ -152,10 +155,12 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
|
||||
self.loadScripts(path)
|
||||
|
||||
## Load all scripts from provided path.
|
||||
# This should probably only be done on init.
|
||||
# \param path Path to check for scripts.
|
||||
def loadScripts(self, path: str) -> None:
|
||||
"""Load all scripts from provided path.
|
||||
|
||||
This should probably only be done on init.
|
||||
:param path: Path to check for scripts.
|
||||
"""
|
||||
|
||||
if ApplicationMetadata.IsEnterpriseVersion:
|
||||
# Delete all __pycache__ not in installation folder, as it may present a security risk.
|
||||
|
@ -173,8 +178,8 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
if not is_in_installation_path:
|
||||
TrustBasics.removeCached(path)
|
||||
|
||||
## Load all scripts in the scripts folders
|
||||
scripts = pkgutil.iter_modules(path = [path])
|
||||
"""Load all scripts in the scripts folders"""
|
||||
for loader, script_name, ispkg in scripts:
|
||||
# Iterate over all scripts.
|
||||
if script_name not in sys.modules:
|
||||
|
@ -278,9 +283,8 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
self.scriptListChanged.emit()
|
||||
self._propertyChanged()
|
||||
|
||||
## When the global container stack is changed, swap out the list of active
|
||||
# scripts.
|
||||
def _onGlobalContainerStackChanged(self) -> None:
|
||||
"""When the global container stack is changed, swap out the list of active scripts."""
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.metaDataChanged.disconnect(self._restoreScriptInforFromMetadata)
|
||||
|
||||
|
@ -323,8 +327,12 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
# We do want to listen to other events.
|
||||
self._global_container_stack.metaDataChanged.connect(self._restoreScriptInforFromMetadata)
|
||||
|
||||
## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection.
|
||||
def _createView(self) -> None:
|
||||
"""Creates the view used by show popup.
|
||||
|
||||
The view is saved because of the fairly aggressive garbage collection.
|
||||
"""
|
||||
|
||||
Logger.log("d", "Creating post processing plugin view.")
|
||||
|
||||
self.loadAllScripts()
|
||||
|
@ -340,8 +348,9 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
# Create the save button component
|
||||
CuraApplication.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton"))
|
||||
|
||||
## Show the (GUI) popup of the post processing plugin.
|
||||
def showPopup(self) -> None:
|
||||
"""Show the (GUI) popup of the post processing plugin."""
|
||||
|
||||
if self._view is None:
|
||||
self._createView()
|
||||
if self._view is None:
|
||||
|
@ -349,11 +358,13 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
return
|
||||
self._view.show()
|
||||
|
||||
## Property changed: trigger re-slice
|
||||
# To do this we use the global container stack propertyChanged.
|
||||
# Re-slicing is necessary for setting changes in this plugin, because the changes
|
||||
# are applied only once per "fresh" gcode
|
||||
def _propertyChanged(self) -> None:
|
||||
"""Property changed: trigger re-slice
|
||||
|
||||
To do this we use the global container stack propertyChanged.
|
||||
Re-slicing is necessary for setting changes in this plugin, because the changes
|
||||
are applied only once per "fresh" gcode
|
||||
"""
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack is not None:
|
||||
global_container_stack.propertyChanged.emit("post_processing_plugin", "value")
|
||||
|
|
|
@ -23,9 +23,10 @@ if TYPE_CHECKING:
|
|||
from UM.Settings.Interfaces import DefinitionContainerInterface
|
||||
|
||||
|
||||
## Base class for scripts. All scripts should inherit the script class.
|
||||
@signalemitter
|
||||
class Script:
|
||||
"""Base class for scripts. All scripts should inherit the script class."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._stack = None # type: Optional[ContainerStack]
|
||||
|
@ -78,13 +79,15 @@ class Script:
|
|||
if global_container_stack is not None:
|
||||
global_container_stack.propertyChanged.emit(key, property_name)
|
||||
|
||||
## Needs to return a dict that can be used to construct a settingcategory file.
|
||||
# See the example script for an example.
|
||||
# It follows the same style / guides as the Uranium settings.
|
||||
# Scripts can either override getSettingData directly, or use getSettingDataString
|
||||
# to return a string that will be parsed as json. The latter has the benefit over
|
||||
# returning a dict in that the order of settings is maintained.
|
||||
def getSettingData(self) -> Dict[str, Any]:
|
||||
"""Needs to return a dict that can be used to construct a settingcategory file.
|
||||
|
||||
See the example script for an example.
|
||||
It follows the same style / guides as the Uranium settings.
|
||||
Scripts can either override getSettingData directly, or use getSettingDataString
|
||||
to return a string that will be parsed as json. The latter has the benefit over
|
||||
returning a dict in that the order of settings is maintained.
|
||||
"""
|
||||
setting_data_as_string = self.getSettingDataString()
|
||||
setting_data = json.loads(setting_data_as_string, object_pairs_hook = collections.OrderedDict)
|
||||
return setting_data
|
||||
|
@ -104,15 +107,18 @@ class Script:
|
|||
return self._stack.getId()
|
||||
return None
|
||||
|
||||
## Convenience function that retrieves value of a setting from the stack.
|
||||
def getSettingValueByKey(self, key: str) -> Any:
|
||||
"""Convenience function that retrieves value of a setting from the stack."""
|
||||
|
||||
if self._stack is not None:
|
||||
return self._stack.getProperty(key, "value")
|
||||
return None
|
||||
|
||||
## Convenience function that finds the value in a line of g-code.
|
||||
# When requesting key = x from line "G1 X100" the value 100 is returned.
|
||||
def getValue(self, line: str, key: str, default = None) -> Any:
|
||||
"""Convenience function that finds the value in a line of g-code.
|
||||
|
||||
When requesting key = x from line "G1 X100" the value 100 is returned.
|
||||
"""
|
||||
if not key in line or (';' in line and line.find(key) > line.find(';')):
|
||||
return default
|
||||
sub_part = line[line.find(key) + 1:]
|
||||
|
@ -127,20 +133,23 @@ class Script:
|
|||
except ValueError: #Not a number at all.
|
||||
return default
|
||||
|
||||
## Convenience function to produce a line of g-code.
|
||||
#
|
||||
# You can put in an original g-code line and it'll re-use all the values
|
||||
# in that line.
|
||||
# All other keyword parameters are put in the result in g-code's format.
|
||||
# For instance, if you put ``G=1`` in the parameters, it will output
|
||||
# ``G1``. If you put ``G=1, X=100`` in the parameters, it will output
|
||||
# ``G1 X100``. The parameters G and M will always be put first. The
|
||||
# parameters T and S will be put second (or first if there is no G or M).
|
||||
# The rest of the parameters will be put in arbitrary order.
|
||||
# \param line The original g-code line that must be modified. If not
|
||||
# provided, an entirely new g-code line will be produced.
|
||||
# \return A line of g-code with the desired parameters filled in.
|
||||
def putValue(self, line: str = "", **kwargs) -> str:
|
||||
"""Convenience function to produce a line of g-code.
|
||||
|
||||
You can put in an original g-code line and it'll re-use all the values
|
||||
in that line.
|
||||
All other keyword parameters are put in the result in g-code's format.
|
||||
For instance, if you put ``G=1`` in the parameters, it will output
|
||||
``G1``. If you put ``G=1, X=100`` in the parameters, it will output
|
||||
``G1 X100``. The parameters G and M will always be put first. The
|
||||
parameters T and S will be put second (or first if there is no G or M).
|
||||
The rest of the parameters will be put in arbitrary order.
|
||||
|
||||
:param line: The original g-code line that must be modified. If not
|
||||
provided, an entirely new g-code line will be produced.
|
||||
:return: A line of g-code with the desired parameters filled in.
|
||||
"""
|
||||
|
||||
#Strip the comment.
|
||||
comment = ""
|
||||
if ";" in line:
|
||||
|
@ -179,7 +188,9 @@ class Script:
|
|||
|
||||
return result
|
||||
|
||||
## This is called when the script is executed.
|
||||
# It gets a list of g-code strings and needs to return a (modified) list.
|
||||
def execute(self, data: List[str]) -> List[str]:
|
||||
"""This is called when the script is executed.
|
||||
|
||||
It gets a list of g-code strings and needs to return a (modified) list.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
|
|
@ -63,10 +63,12 @@ class FilamentChange(Script):
|
|||
}
|
||||
}"""
|
||||
|
||||
## Inserts the filament change g-code at specific layer numbers.
|
||||
# \param data A list of layers of g-code.
|
||||
# \return A similar list, with filament change commands inserted.
|
||||
def execute(self, data: List[str]):
|
||||
"""Inserts the filament change g-code at specific layer numbers.
|
||||
|
||||
:param data: A list of layers of g-code.
|
||||
:return: A similar list, with filament change commands inserted.
|
||||
"""
|
||||
layer_nums = self.getSettingValueByKey("layer_number")
|
||||
initial_retract = self.getSettingValueByKey("initial_retract")
|
||||
later_retract = self.getSettingValueByKey("later_retract")
|
||||
|
|
|
@ -134,9 +134,8 @@ class PauseAtHeight(Script):
|
|||
}
|
||||
}"""
|
||||
|
||||
## Get the X and Y values for a layer (will be used to get X and Y of the
|
||||
# layer after the pause).
|
||||
def getNextXY(self, layer: str) -> Tuple[float, float]:
|
||||
"""Get the X and Y values for a layer (will be used to get X and Y of the layer after the pause)."""
|
||||
lines = layer.split("\n")
|
||||
for line in lines:
|
||||
if self.getValue(line, "X") is not None and self.getValue(line, "Y") is not None:
|
||||
|
@ -145,10 +144,12 @@ class PauseAtHeight(Script):
|
|||
return x, y
|
||||
return 0, 0
|
||||
|
||||
## Inserts the pause commands.
|
||||
# \param data: List of layers.
|
||||
# \return New list of layers.
|
||||
def execute(self, data: List[str]) -> List[str]:
|
||||
"""Inserts the pause commands.
|
||||
|
||||
:param data: List of layers.
|
||||
:return: New list of layers.
|
||||
"""
|
||||
pause_at = self.getSettingValueByKey("pause_at")
|
||||
pause_height = self.getSettingValueByKey("pause_height")
|
||||
pause_layer = self.getSettingValueByKey("pause_layer")
|
||||
|
|
|
@ -5,8 +5,10 @@ import math
|
|||
|
||||
from ..Script import Script
|
||||
|
||||
## Continues retracting during all travel moves.
|
||||
|
||||
class RetractContinue(Script):
|
||||
"""Continues retracting during all travel moves."""
|
||||
|
||||
def getSettingDataString(self):
|
||||
return """{
|
||||
"name": "Retract Continue",
|
||||
|
|
|
@ -5,11 +5,14 @@ import re #To perform the search and replace.
|
|||
|
||||
from ..Script import Script
|
||||
|
||||
## Performs a search-and-replace on all g-code.
|
||||
#
|
||||
# Due to technical limitations, the search can't cross the border between
|
||||
# layers.
|
||||
|
||||
class SearchAndReplace(Script):
|
||||
"""Performs a search-and-replace on all g-code.
|
||||
|
||||
Due to technical limitations, the search can't cross the border between
|
||||
layers.
|
||||
"""
|
||||
|
||||
def getSettingDataString(self):
|
||||
return """{
|
||||
"name": "Search and Replace",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue