Skip to content

Commit

Permalink
BUG: Make Segment Editor effect names translatable
Browse files Browse the repository at this point in the history
Segment Editor effects have now separate "name" (for modules to refer to the effect) and "title" (displayed on the GUI).

This partially fixes these issues:
- Slicer#6177
- Slicer/SlicerLanguagePacks#12
  • Loading branch information
mhdiop authored and lassoan committed Oct 3, 2023
1 parent 31fe28e commit f246ab1
Show file tree
Hide file tree
Showing 40 changed files with 500 additions and 395 deletions.
26 changes: 23 additions & 3 deletions Base/Python/slicer/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,17 @@ def translate(context, text):


def getContext(sourceFile):
"""Get the translation context based on the source file name."""
"""Get the translation context name.
The context name is constructed from the Python package name (name of the parent folder that contains `__init__.py` file - if it exists)
and the source file name (without the `.py` extension).
Most Slicer modules do not have a `__init__.py` file in their folder, so the context name is simply the source file name
(for example, `DICOMEnhancedUSVolumePlugin`).
Most helper Python scripts in Slicer are Python packages (subfolders containing addition Python scripts and an `__init__.py` file)
and their name is constructed as PythonPackageName.SourceFileName (for example, `SegmentEditorEffects.SegmentEditorDrawEffect`).
"""
if os.path.isfile(sourceFile):
parentFolder = os.path.dirname(sourceFile)
init_file_path = parentFolder + os.path.sep + '__init__.py'
Expand All @@ -28,8 +38,18 @@ def getContext(sourceFile):


def tr(text):
"""Translation function for python scripted modules that gets context name from filename.
Experimental, not used yet."""
"""Translation function for python scripted modules that automatically determines context name.
This is more convenient to use than `translate(context, text)` because the developer does not need to manually specify the context.
This function is typically imported as `_` function.
Example::
from slicer.i18n import tr as _
...
statusText = _("Idle") if idle else _("Running")
"""
filename = inspect.stack()[1][1]
contextName = getContext(filename)
return translate(contextName, text)
2 changes: 1 addition & 1 deletion Modules/Loadable/Models/qSlicerModelsModule.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ QString qSlicerModelsModule::helpText()const
//-----------------------------------------------------------------------------
QString qSlicerModelsModule::acknowledgementText()const
{
return "This work was partially funded by NIH grants 3P41RR013218-12S1 and R01CA184354.";
return tr("This work was partially funded by NIH grants 3P41RR013218-12S1 and R01CA184354.");
}

//-----------------------------------------------------------------------------
Expand Down
51 changes: 5 additions & 46 deletions Modules/Loadable/Segmentations/EditorEffects/Python/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,47 +1,6 @@
#-----------------------------------------------------------------------------
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/SegmentEditorEffects.__init__.py.in
${CMAKE_BINARY_DIR}/${Slicer_QTSCRIPTEDMODULES_LIB_DIR}/SegmentEditorEffects/__init__.py
)
# Note: The SegmentEditorEffects subdirectory is needed to have a Python package
# (https://docs.python.org/3/tutorial/modules.html#packages) in the source tree
# the same way as it is in the install tree. The Python package name is used
# for determining the "context" string (packageName.filename) for language translation (i18n).

#-----------------------------------------------------------------------------
set(SegmentEditorEffects_PYTHON_SCRIPTS
AbstractScriptedSegmentEditorEffect
AbstractScriptedSegmentEditorLabelEffect
AbstractScriptedSegmentEditorPaintEffect
AbstractScriptedSegmentEditorAutoCompleteEffect
SegmentEditorDrawEffect
SegmentEditorFillBetweenSlicesEffect
SegmentEditorGrowFromSeedsEffect
SegmentEditorHollowEffect
SegmentEditorIslandsEffect
SegmentEditorLevelTracingEffect
SegmentEditorLogicalEffect
SegmentEditorMarginEffect
SegmentEditorMaskVolumeEffect
SegmentEditorSmoothingEffect
SegmentEditorThresholdEffect
)

set(SegmentEditorEffects_PYTHON_RESOURCES
Resources/Icons/Draw.png
Resources/Icons/FillBetweenSlices.png
Resources/Icons/GrowFromSeeds.png
Resources/Icons/Hollow.png
Resources/Icons/Islands.png
Resources/Icons/LevelTracing.png
Resources/Icons/Logical.png
Resources/Icons/Margin.png
Resources/Icons/MaskVolume.png
Resources/Icons/Smoothing.png
Resources/Icons/Threshold.png
)

ctkMacroCompilePythonScript(
TARGET_NAME SegmentEditorEffects
SCRIPTS "${SegmentEditorEffects_PYTHON_SCRIPTS}"
RESOURCES "${SegmentEditorEffects_PYTHON_RESOURCES}"
DESTINATION_DIR ${Slicer_BINARY_DIR}/${Slicer_QTSCRIPTEDMODULES_LIB_DIR}/SegmentEditorEffects
INSTALL_DIR ${Slicer_INSTALL_QTSCRIPTEDMODULES_LIB_DIR}/SegmentEditorEffects
NO_INSTALL_SUBDIR
)
add_subdirectory(SegmentEditorEffects)

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import vtk

import slicer
from slicer.i18n import tr as _

from .AbstractScriptedSegmentEditorEffect import *

Expand Down Expand Up @@ -86,14 +87,14 @@ def isBackgroundLabelmap(labelmapOrientedImageData, label=None):
return False

def setupOptionsFrame(self):
self.autoUpdateCheckBox = qt.QCheckBox("Auto-update")
self.autoUpdateCheckBox.setToolTip("Auto-update results preview when input segments change.")
self.autoUpdateCheckBox = qt.QCheckBox(_("Auto-update"))
self.autoUpdateCheckBox.setToolTip(_("Auto-update results preview when input segments change."))
self.autoUpdateCheckBox.setChecked(True)
self.autoUpdateCheckBox.setEnabled(False)

self.previewButton = qt.QPushButton("Initialize")
self.previewButton = qt.QPushButton(_("Initialize"))
self.previewButton.objectName = self.__class__.__name__ + 'Preview'
self.previewButton.setToolTip("Preview complete segmentation")
self.previewButton.setToolTip(_("Preview complete segmentation"))
# qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
# fails on some systems, therefore set the policies using separate method calls
qSize = qt.QSizePolicy()
Expand All @@ -103,35 +104,35 @@ def setupOptionsFrame(self):
previewFrame = qt.QHBoxLayout()
previewFrame.addWidget(self.autoUpdateCheckBox)
previewFrame.addWidget(self.previewButton)
self.scriptedEffect.addLabeledOptionsWidget("Preview:", previewFrame)
self.scriptedEffect.addLabeledOptionsWidget(_("Preview:"), previewFrame)

self.previewOpacitySlider = ctk.ctkSliderWidget()
self.previewOpacitySlider.setToolTip("Adjust visibility of results preview.")
self.previewOpacitySlider.setToolTip(_("Adjust visibility of results preview."))
self.previewOpacitySlider.minimum = 0
self.previewOpacitySlider.maximum = 1.0
self.previewOpacitySlider.value = 0.0
self.previewOpacitySlider.singleStep = 0.05
self.previewOpacitySlider.pageStep = 0.1
self.previewOpacitySlider.spinBoxVisible = False

self.previewShow3DButton = qt.QPushButton("Show 3D")
self.previewShow3DButton.setToolTip("Preview results in 3D.")
self.previewShow3DButton = qt.QPushButton(_("Show 3D"))
self.previewShow3DButton.setToolTip(_("Preview results in 3D."))
self.previewShow3DButton.setCheckable(True)

displayFrame = qt.QHBoxLayout()
displayFrame.addWidget(qt.QLabel("inputs"))
displayFrame.addWidget(qt.QLabel(_("inputs")))
displayFrame.addWidget(self.previewOpacitySlider)
displayFrame.addWidget(qt.QLabel("results"))
displayFrame.addWidget(qt.QLabel(_("results")))
displayFrame.addWidget(self.previewShow3DButton)
self.scriptedEffect.addLabeledOptionsWidget("Display:", displayFrame)
self.scriptedEffect.addLabeledOptionsWidget(_("Display:"), displayFrame)

self.cancelButton = qt.QPushButton("Cancel")
self.cancelButton = qt.QPushButton(_("Cancel"))
self.cancelButton.objectName = self.__class__.__name__ + 'Cancel'
self.cancelButton.setToolTip("Clear preview and cancel auto-complete")
self.cancelButton.setToolTip(_("Clear preview and cancel auto-complete"))

self.applyButton = qt.QPushButton("Apply")
self.applyButton = qt.QPushButton(_("Apply"))
self.applyButton.objectName = self.__class__.__name__ + 'Apply'
self.applyButton.setToolTip("Replace segments by previewed result")
self.applyButton.setToolTip(_("Replace segments by previewed result"))

finishFrame = qt.QHBoxLayout()
finishFrame.addWidget(self.cancelButton)
Expand Down Expand Up @@ -244,13 +245,13 @@ def updateGUIFromMRML(self):
wasBlocked = self.previewOpacitySlider.blockSignals(True)
self.previewOpacitySlider.value = self.getPreviewOpacity()
self.previewOpacitySlider.blockSignals(wasBlocked)
self.previewButton.text = "Update"
self.previewButton.text = _("Update")
self.previewShow3DButton.setEnabled(True)
self.previewShow3DButton.setChecked(self.getPreviewShow3D())
self.autoUpdateCheckBox.setEnabled(True)
self.observeSegmentation(self.autoUpdateCheckBox.isChecked())
else:
self.previewButton.text = "Initialize"
self.previewButton.text = _("Initialize")
self.autoUpdateCheckBox.setEnabled(False)
self.previewShow3DButton.setEnabled(False)
self.delayedAutoUpdateTimer.stop()
Expand All @@ -276,7 +277,7 @@ def onPreview(self):
return
self.previewComputationInProgress = True

slicer.util.showStatusMessage(f"Running {self.scriptedEffect.name} auto-complete...", 2000)
slicer.util.showStatusMessage(_("Running {effectName} auto-complete...").format(effectName=self.scriptedEffect.name), 2000)
try:
# This can be a long operation - indicate it to the user
qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
Expand Down Expand Up @@ -404,7 +405,8 @@ def effectiveExtentChanged(self):
# Current extent used for auto-complete preview
currentLabelExtent = self.mergedLabelmapGeometryImage.GetExtent()

# Determine if the current merged labelmap extent has less than a 3 voxel margin around the effective segment extent (limited by the master image extent)
# Determine if the current merged labelmap extent has less than a 3 voxel margin around the effective segment extent
# (limited by the source image extent)
return ((masterImageExtent[0] != currentLabelExtent[0] and currentLabelExtent[0] > effectiveLabelExtent[0] - self.minimumExtentMargin) or
(masterImageExtent[1] != currentLabelExtent[1] and currentLabelExtent[1] < effectiveLabelExtent[1] + self.minimumExtentMargin) or
(masterImageExtent[2] != currentLabelExtent[2] and currentLabelExtent[2] > effectiveLabelExtent[2] - self.minimumExtentMargin) or
Expand Down Expand Up @@ -522,8 +524,9 @@ def preview(self):
# as the closed surfaces will be converted as necessary by the segmentation logic.

mergedImage = slicer.vtkOrientedImageData()
segmentationNode.GenerateMergedLabelmapForAllSegments(mergedImage,
vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_EFFECTIVE_SEGMENTS, self.mergedLabelmapGeometryImage, self.selectedSegmentIds)
segmentationNode.GenerateMergedLabelmapForAllSegments(
mergedImage,
vtkSegmentationCore.vtkSegmentation.EXTENT_UNION_OF_EFFECTIVE_SEGMENTS, self.mergedLabelmapGeometryImage, self.selectedSegmentIds)

outputLabelmap = slicer.vtkOrientedImageData()
self.computePreviewLabelmap(mergedImage, outputLabelmap)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#-----------------------------------------------------------------------------
set(SegmentEditorEffects_PYTHON_SCRIPTS
__init__
AbstractScriptedSegmentEditorEffect
AbstractScriptedSegmentEditorLabelEffect
AbstractScriptedSegmentEditorPaintEffect
AbstractScriptedSegmentEditorAutoCompleteEffect
SegmentEditorDrawEffect
SegmentEditorFillBetweenSlicesEffect
SegmentEditorGrowFromSeedsEffect
SegmentEditorHollowEffect
SegmentEditorIslandsEffect
SegmentEditorLevelTracingEffect
SegmentEditorLogicalEffect
SegmentEditorMarginEffect
SegmentEditorMaskVolumeEffect
SegmentEditorSmoothingEffect
SegmentEditorThresholdEffect
)

set(SegmentEditorEffects_PYTHON_RESOURCES
Resources/Icons/Draw.png
Resources/Icons/FillBetweenSlices.png
Resources/Icons/GrowFromSeeds.png
Resources/Icons/Hollow.png
Resources/Icons/Islands.png
Resources/Icons/LevelTracing.png
Resources/Icons/Logical.png
Resources/Icons/Margin.png
Resources/Icons/MaskVolume.png
Resources/Icons/Smoothing.png
Resources/Icons/Threshold.png
)

ctkMacroCompilePythonScript(
TARGET_NAME SegmentEditorEffects
SCRIPTS "${SegmentEditorEffects_PYTHON_SCRIPTS}"
RESOURCES "${SegmentEditorEffects_PYTHON_RESOURCES}"
DESTINATION_DIR ${Slicer_BINARY_DIR}/${Slicer_QTSCRIPTEDMODULES_LIB_DIR}/SegmentEditorEffects
INSTALL_DIR ${Slicer_INSTALL_QTSCRIPTEDMODULES_LIB_DIR}/SegmentEditorEffects
NO_INSTALL_SUBDIR
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import vtk

import slicer
from slicer.i18n import tr as _

from SegmentEditorEffects import *

Expand All @@ -15,7 +16,8 @@ class SegmentEditorDrawEffect(AbstractScriptedSegmentEditorLabelEffect):
"""

def __init__(self, scriptedEffect):
scriptedEffect.name = 'Draw'
scriptedEffect.name = 'Draw' # no tr (don't translate it because modules find effects by name)
scriptedEffect.title = _('Draw')
self.drawPipelines = {}
AbstractScriptedSegmentEditorLabelEffect.__init__(self, scriptedEffect)

Expand All @@ -32,13 +34,13 @@ def icon(self):
return qt.QIcon()

def helpText(self):
return """<html>Draw segment outline in slice viewers<br>.
return "<html>" + _("""Draw segment outline in slice viewers<br>.
<p><ul style="margin: 0">
<li><b>Left-click:</b> add point.</li>
<li><b>Left-button drag-and-drop:</b> add multiple points.</li>
<li><b>x:</b> delete last point.</li>
<li><b>Double-left-click</b> or <b>right-click</b> or <b>a</b> or <b>enter</b>: apply outline.</li>
</ul><p></html>"""
<li><b>Left-click:</b> add point.
<li><b>Left-button drag-and-drop:</b> add multiple points.
<li><b>x:</b> delete last point.
<li><b>Double-left-click</b> or <b>right-click</b> or <b>a</b> or <b>enter</b>: apply outline.
</ul><p>""")

def deactivate(self):
# Clear draw pipelines
Expand Down Expand Up @@ -92,7 +94,8 @@ def processInteractionEvents(self, callerInteractor, eventId, viewWidget):
sliceNode = viewWidget.sliceLogic().GetSliceNode()
pipeline.lastInsertSliceNodeMTime = sliceNode.GetMTime()
abortEvent = True
elif (eventId == vtk.vtkCommand.RightButtonReleaseEvent and pipeline.actionState == "finishing") or (eventId == vtk.vtkCommand.LeftButtonDoubleClickEvent and not anyModifierKeyPressed):
elif ((eventId == vtk.vtkCommand.RightButtonReleaseEvent and pipeline.actionState == "finishing")
or (eventId == vtk.vtkCommand.LeftButtonDoubleClickEvent and not anyModifierKeyPressed)):
abortEvent = (pipeline.rasPoints.GetNumberOfPoints() > 1)
sliceNode = viewWidget.sliceLogic().GetSliceNode()
if abs(pipeline.lastInsertSliceNodeMTime - sliceNode.GetMTime()) < 2:
Expand Down
Loading

0 comments on commit f246ab1

Please sign in to comment.