diff --git a/fsl/data/strings.py b/fsl/data/strings.py index d6e99e50a97006a685b94b2539adb143b8e662d5..4f3673fd93687474de6c91872f6b4c67cf2ae179 100644 --- a/fsl/data/strings.py +++ b/fsl/data/strings.py @@ -506,6 +506,7 @@ properties = TypeDict({ 'VolumeOpts.displayRange' : 'Display range', 'VolumeOpts.clippingRange' : 'Clipping range', + 'VolumeOpts.centreRanges' : 'Centre display/clipping ranges at 0', '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 d1aa45915c58302789bdc88db5e0ea9f81831b6b..e8e44256a1d6cf654aad4d064715ffa52949699e 100644 --- a/fsl/fsleyes/controls/overlaydisplaypanel.py +++ b/fsl/fsleyes/controls/overlaydisplaypanel.py @@ -288,6 +288,7 @@ _DISPLAY_PROPS = td.TypeDict({ props.Widget('cmap'), props.Widget('invert'), props.Widget('invertClipping'), + props.Widget('centreRanges'), props.Widget('displayRange', showLimits=False, slider=True, diff --git a/fsl/fsleyes/displaycontext/volumeopts.py b/fsl/fsleyes/displaycontext/volumeopts.py index 3f6702d11ce641fd000a60ddeb68dd8b7bee1a13..005cce6efcd3af6ca20a161842edd50a4040f81c 100644 --- a/fsl/fsleyes/displaycontext/volumeopts.py +++ b/fsl/fsleyes/displaycontext/volumeopts.py @@ -502,6 +502,13 @@ class VolumeOpts(ImageOpts): """ + centreRanges = props.Boolean(default=False) + """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. + """ + + def __init__(self, overlay, display, @@ -572,6 +579,9 @@ class VolumeOpts(ImageOpts): self .addListener('displayRange', self.name, self.__displayRangeChanged) + self .addListener('centreRanges', + self.name, + self.__centreRangesChanged) # Because displayRange and bri/con are intrinsically # linked, it makes no sense to let the user sync/unsync @@ -646,6 +656,11 @@ class VolumeOpts(ImageOpts): for peer in peers: + if not any((peer.display.isSyncedToParent('brightness'), + peer.display.isSyncedToParent('contrast'), + peer. isSyncedToParent('displayRange'))): + continue + if enable: peer.display.enableListener('brightness', peer.name) peer.display.enableListener('contrast', peer.name) @@ -684,6 +699,9 @@ class VolumeOpts(ImageOpts): See :func:`.colourmaps.displayRangeToBricon`. """ + if self.centreRanges: + return + brightness, contrast = fslcm.displayRangeToBricon( (self.dataMin, self.dataMax), self.displayRange.x) @@ -695,3 +713,83 @@ class VolumeOpts(ImageOpts): self.display.contrast = contrast * 100 self.__toggleListeners(True) + + + def __centreRangesChanged(self, *a): + """Called when the :attr:`centreRanges` property changes. Configures + property listeners on the :attr:`clippingRange` and + :attr:`displayRange` properties. + """ + + if self.centreRanges: + self.display.disableProperty('brightness') + self.display.disableProperty('contrast') + else: + self.display.enableProperty('brightness') + self.display.enableProperty('contrast') + + clipPVs = self.clippingRange.getPropertyValueList() + dispPVs = self.displayRange .getPropertyValueList() + + if not self.centreRanges: + clipPVs[0].removeListener(self.name) + clipPVs[1].removeListener(self.name) + dispPVs[0].removeListener(self.name) + dispPVs[1].removeListener(self.name) + else: + clipPVs[0].addListener(self.name, self.__lowClippingChanged) + clipPVs[1].addListener(self.name, self.__highClippingChanged) + dispPVs[0].addListener(self.name, self.__lowDisplayChanged) + dispPVs[1].addListener(self.name, self.__highDisplayChanged) + + self.__lowClippingChanged() + self.__lowDisplayChanged() + + + def __lowDisplayChanged(self, *a): + """If :attr:`centreRanges` is ``True``, this method is called whenever + the low :attr:`displayRange` value changes. It synchronises the high + value. + """ + rangePVs = self.displayRange.getPropertyValueList() + + rangePVs[1].disableListener(self.name) + rangePVs[1].set(-rangePVs[0].get()) + rangePVs[1].enableListener(self.name) + + + def __highDisplayChanged(self, *a): + """If :attr:`centreRanges` is ``True``, this method is called whenever + the high :attr:`displayRange` value changes. It synchronises the low + value. + """ + rangePVs = self.displayRange.getPropertyValueList() + + rangePVs[0].disableListener(self.name) + rangePVs[0].set(-rangePVs[1].get()) + rangePVs[0].enableListener(self.name) + + + def __lowClippingChanged(self, *a): + """If :attr:`centreRanges` is ``True``, this method is called whenever + the low :attr:`clippingRange` value changes. It synchronises the high + value. + """ + + clipPVs = self.clippingRange.getPropertyValueList() + + clipPVs[1].disableListener(self.name) + clipPVs[1].set(-clipPVs[0].get()) + clipPVs[1].enableListener(self.name) + + + def __highClippingChanged(self, *a): + """If :attr:`centreRanges` is ``True``, this method is called whenever + the high :attr:`clippingRange` value changes. It synchronises the low + value. + """ + clipPVs = self.clippingRange.getPropertyValueList() + + clipPVs[0].disableListener(self.name) + clipPVs[0].set(-clipPVs[1].get()) + clipPVs[0].enableListener(self.name) diff --git a/fsl/fsleyes/tooltips.py b/fsl/fsleyes/tooltips.py index 8d223ad81420d57ed94441f706bd49b994e13533..d60cf1c8ee6211ac5fe0f5f71bee8e90f4c760a4 100644 --- a/fsl/fsleyes/tooltips.py +++ b/fsl/fsleyes/tooltips.py @@ -67,6 +67,11 @@ properties = TypeDict({ 'voxels outside of the range are displayed.' 'This option is useful for displaying ' 'statistic images.', + 'VolumeOpts.centreRanges' : 'If checked, the low and high values ' + 'of both the clipping and display ' + 'ranges will be yoked together, so ' + 'that both ranges stay centered at ' + 'zero.' , 'VolumeOpts.cmap' : 'The colour map to use.', 'VolumeOpts.interpolation' : 'Interpolate the image data for display ' 'purposes. You can choose no '