diff --git a/fsl/data/strings.py b/fsl/data/strings.py index 4f3673fd93687474de6c91872f6b4c67cf2ae179..851b74d4b73780a872c9a2883801bb5d3e30c054 100644 --- a/fsl/data/strings.py +++ b/fsl/data/strings.py @@ -507,6 +507,8 @@ properties = TypeDict({ 'VolumeOpts.displayRange' : 'Display range', 'VolumeOpts.clippingRange' : 'Clipping range', 'VolumeOpts.centreRanges' : 'Centre display/clipping ranges at 0', + 'VolumeOpts.linkLowRanges' : 'Link low display/clipping ranges', + 'VolumeOpts.linkHighRanges' : 'Link high display/clipping ranges', 'VolumeOpts.cmap' : 'Colour map', 'VolumeOpts.invert' : 'Invert colour map', 'VolumeOpts.invertClipping' : 'Invert clipping range', diff --git a/fsl/fsleyes/controls/overlaydisplaypanel.py b/fsl/fsleyes/controls/overlaydisplaypanel.py index e8e44256a1d6cf654aad4d064715ffa52949699e..afc1dd207221330bc561991c1e5753472c2070a0 100644 --- a/fsl/fsleyes/controls/overlaydisplaypanel.py +++ b/fsl/fsleyes/controls/overlaydisplaypanel.py @@ -207,7 +207,8 @@ class OverlayDisplayPanel(fslpanel.FSLEyesPanel): self.__widgets.ClearGroup(groupName) dispProps = _DISPLAY_PROPS[target] - labels = [strings.properties[target, p.key] for p in dispProps] + labels = [strings.properties.get((target, p.key), p.key) + for p in dispProps] tooltips = [fsltooltips.properties.get((target, p.key), None) for p in dispProps] @@ -289,6 +290,8 @@ _DISPLAY_PROPS = td.TypeDict({ props.Widget('invert'), props.Widget('invertClipping'), props.Widget('centreRanges'), + props.Widget('linkLowRanges'), + props.Widget('linkHighRanges'), props.Widget('displayRange', showLimits=False, slider=True, diff --git a/fsl/fsleyes/displaycontext/volumeopts.py b/fsl/fsleyes/displaycontext/volumeopts.py index d6f5f8bd892dfee9a2ce6d84643efd81f65a7db7..f7199d85bf0ee06239a281355fa867ced06b025b 100644 --- a/fsl/fsleyes/displaycontext/volumeopts.py +++ b/fsl/fsleyes/displaycontext/volumeopts.py @@ -506,8 +506,26 @@ class VolumeOpts(ImageOpts): """If ``True``, the :attr:`displayRange` and :attr:`clippingRange` ranges will be kept centred at zero. A change to the negative end of either range will result in the positive end being changed, and vice versa. + + .. note:: When this property is set to ``True``, the + :attr:`.Display.brightness`, :attr:`.Display.contrast`, + :attr:`.linkLowRanges` and :attr:`.linkHighRanges` properties + are all disabled, as managing the interaction between all of + them would be far too complicated. + """ + + + linkLowRanges = props.Boolean(default=False) + """If ``True``, the low bounds on both the :attr:`displayRange` and + :attr:`clippingRange` ranges will be linked together. """ + + linkHighRanges = props.Boolean(default=False) + """If ``True``, the high bounds on both the :attr:`displayRange` and + :attr:`clippingRange` ranges will be linked together. + """ + def __init__(self, overlay, @@ -560,7 +578,8 @@ class VolumeOpts(ImageOpts): self.displayRange.xmin = self.dataMin - 10 * dRangeLen self.displayRange.xmax = self.dataMax + 10 * dRangeLen - self.setConstraint('displayRange', 'minDistance', dMinDistance) + self.setConstraint('displayRange', 'minDistance', dMinDistance) + self.setConstraint('clippingRange', 'minDistance', dMinDistance) ImageOpts.__init__(self, overlay, @@ -581,7 +600,13 @@ class VolumeOpts(ImageOpts): self.__displayRangeChanged) self .addListener('centreRanges', self.name, - self.__centreRangesChanged) + self.__centreRangesChanged) + self .addListener('linkLowRanges', + self.name, + self.__linkLowRangesChanged) + self .addListener('linkHighRanges', + self.name, + self.__linkHighRangesChanged) # Because displayRange and bri/con are intrinsically # linked, it makes no sense to let the user sync/unsync @@ -722,13 +747,56 @@ class VolumeOpts(ImageOpts): """ if self.centreRanges: + self.linkLowRanges = False + self.linkHighRanges = False + self.display.disableProperty('brightness') self.display.disableProperty('contrast') + self .disableProperty('linkLowRanges') + self .disableProperty('linkHighRanges') self.setConstraint('displayRange', 'dimCentres', [0.0]) self.setConstraint('clippingRange', 'dimCentres', [0.0]) else: self.display.enableProperty('brightness') self.display.enableProperty('contrast') + self .enableProperty('linkLowRanges') + self .enableProperty('linkHighRanges') self.setConstraint('displayRange', 'dimCentres', [None]) self.setConstraint('clippingRange', 'dimCentres', [None]) + + + def __linkLowRangesChanged(self, *a): + """Called when the :attr:`linkLowRanges` property changes. Calls the + :meth:`__linkRangesChanged` method. + """ + self.__linkRangesChanged(self.linkLowRanges, 0) + + + def __linkHighRangesChanged(self, *a): + """Called when the :attr:`linkHighRanges` property changes. Calls the + :meth:`__linkRangesChanged` method. + """ + self.__linkRangesChanged(self.linkHighRanges, 1) + + + def __linkRangesChanged(self, val, idx): + """Called when either the :attr:`linkLowRanges` or + :attr:`linkHighRanges` properties change. Binds/unbinds the specified + range properties together. + + :arg val: Boolean indicating whether the range values should be + linked or unlinked. + + :arg idx: Range value index - 0 corresponds to the low range value, + and 1 to the high range value. + """ + + dRangePVs = self.displayRange .getPropertyValueList() + cRangePVs = self.clippingRange.getPropertyValueList() + + props.bindPropVals(dRangePVs[idx], + cRangePVs[idx], + bindval=True, + bindatt=False, + unbind=not val)