Skip to content
Snippets Groups Projects
Commit b0c69ede authored by Paul McCarthy's avatar Paul McCarthy
Browse files

Move strings module from fsl.fslview into fsl.data package, as it needs

to be available to non-fslview things. Moved orientation constants
from fsl.data.image to (new module) fsl.data.constants, so circular
imports are not necessary.

There could still be some bugs lurking ..
parent 792c5a74
No related branches found
No related tags found
No related merge requests found
#!/usr/bin/env python
#
# constants.py -
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
# Constants which represent the orientation
# of an axis, in either voxel or world space.
ORIENT_UNKNOWN = -1
ORIENT_L2R = 0
ORIENT_R2L = 1
ORIENT_P2A = 2
ORIENT_A2P = 3
ORIENT_I2S = 4
ORIENT_S2I = 5
# Constants from the NIFTI1 specification that define
# the 'space' in which an image is assumed to be.
NIFTI_XFORM_UNKNOWN = 0
NIFTI_XFORM_SCANNER_ANAT = 1
NIFTI_XFORM_ALIGNED_ANAT = 2
NIFTI_XFORM_TALAIRACH = 3
NIFTI_XFORM_MNI_152 = 4
...@@ -23,30 +23,12 @@ import props ...@@ -23,30 +23,12 @@ import props
import fsl.utils.transform as transform import fsl.utils.transform as transform
import fsl.data.imageio as iio import fsl.data.imageio as iio
import fsl.data.constants as constants
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# Constants which represent the orientation
# of an axis, in either voxel or world space.
ORIENT_UNKNOWN = -1
ORIENT_L2R = 0
ORIENT_R2L = 1
ORIENT_P2A = 2
ORIENT_A2P = 3
ORIENT_I2S = 4
ORIENT_S2I = 5
# Constants from the NIFTI1 specification that define
# the 'space' in which an image is assumed to be.
NIFTI_XFORM_UNKNOWN = 0
NIFTI_XFORM_SCANNER_ANAT = 1
NIFTI_XFORM_ALIGNED_ANAT = 2
NIFTI_XFORM_TALAIRACH = 3
NIFTI_XFORM_MNI_152 = 4
class Image(props.HasProperties): class Image(props.HasProperties):
"""Class which represents a 3D/4D image. Internally, the image is """Class which represents a 3D/4D image. Internally, the image is
...@@ -251,8 +233,8 @@ class Image(props.HasProperties): ...@@ -251,8 +233,8 @@ class Image(props.HasProperties):
sform_code = self.nibImage.get_header()['sform_code'] sform_code = self.nibImage.get_header()['sform_code']
# Invalid values # Invalid values
if sform_code > 4: code = NIFTI_XFORM_UNKNOWN if sform_code > 4: code = constants.NIFTI_XFORM_UNKNOWN
elif sform_code < 0: code = NIFTI_XFORM_UNKNOWN elif sform_code < 0: code = constants.NIFTI_XFORM_UNKNOWN
# All is well # All is well
else: code = sform_code else: code = sform_code
...@@ -283,12 +265,12 @@ class Image(props.HasProperties): ...@@ -283,12 +265,12 @@ class Image(props.HasProperties):
to superior). to superior).
""" """
if self.getXFormCode() == NIFTI_XFORM_UNKNOWN: if self.getXFormCode() == constants.NIFTI_XFORM_UNKNOWN:
return -1 return -1
if axis == 0: return ORIENT_L2R if axis == 0: return constants.ORIENT_L2R
elif axis == 1: return ORIENT_P2A elif axis == 1: return constants.ORIENT_P2A
elif axis == 2: return ORIENT_I2S elif axis == 2: return constants.ORIENT_I2S
else: return -1 else: return -1
...@@ -301,16 +283,17 @@ class Image(props.HasProperties): ...@@ -301,16 +283,17 @@ class Image(props.HasProperties):
of the return value. of the return value.
""" """
if self.getXFormCode() == NIFTI_XFORM_UNKNOWN: if self.getXFormCode() == constants.NIFTI_XFORM_UNKNOWN:
return -1 return -1
# the aff2axcodes returns one code for each # the aff2axcodes returns one code for each
# axis in the image array (i.e. in voxel space), # axis in the image array (i.e. in voxel space),
# which denotes the real world direction # which denotes the real world direction
code = nib.orientations.aff2axcodes(self.nibImage.get_affine(), code = nib.orientations.aff2axcodes(
((ORIENT_R2L, ORIENT_L2R), self.nibImage.get_affine(),
(ORIENT_A2P, ORIENT_P2A), ((constants.ORIENT_R2L, constants.ORIENT_L2R),
(ORIENT_S2I, ORIENT_I2S)))[axis] (constants.ORIENT_A2P, constants.ORIENT_P2A),
(constants.ORIENT_S2I, constants.ORIENT_I2S)))[axis]
return code return code
......
...@@ -5,15 +5,16 @@ ...@@ -5,15 +5,16 @@
# Author: Paul McCarthy <pauldmccarthy@gmail.com> # Author: Paul McCarthy <pauldmccarthy@gmail.com>
# #
import logging import logging
import os import os
import os.path as op import os.path as op
import subprocess as sp import subprocess as sp
import tempfile import tempfile
import nibabel as nib import nibabel as nib
import image as fslimage import fsl.data.strings as strings
import image as fslimage
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -29,13 +30,11 @@ if if the ``allowedExts`` parameter is not passed to any of the functions in ...@@ -29,13 +30,11 @@ if if the ``allowedExts`` parameter is not passed to any of the functions in
this module. this module.
""" """
ALLOWED_EXTENSIONS = [';'.join(ALLOWED_EXTENSIONS)] + ALLOWED_EXTENSIONS EXTENSION_DESCRIPTIONS = ['Compressed NIFTI1 images',
EXTENSION_DESCRIPTIONS = ['All supported files'
'Compressed NIFTI1 images',
'NIFTI1 images', 'NIFTI1 images',
'ANALYZE75 images', 'ANALYZE75 images',
'NIFTI1/ANALYZE75 headers', 'NIFTI1/ANALYZE75 headers',
'Compressed NIFTI1/ANALYZE75 images' 'Compressed NIFTI1/ANALYZE75 images',
'Compressed images'] 'Compressed images']
"""Descriptions for each of the extensions in :data:`ALLOWED_EXTENSIONS`. """ """Descriptions for each of the extensions in :data:`ALLOWED_EXTENSIONS`. """
...@@ -58,9 +57,12 @@ def makeWildcard(allowedExts=None): ...@@ -58,9 +57,12 @@ def makeWildcard(allowedExts=None):
else: else:
descs = allowedExts descs = allowedExts
exts = ['*{}'.format(ext) for ext in allowedExts] exts = ['*{}'.format(ext) for ext in allowedExts]
exts = [';'.join(exts)] + exts
descs = ['All supported files'] + descs
wcParts = ['|'.join((desc, ext)) for (desc, ext) in zip(descs, exts)] wcParts = ['|'.join((desc, ext)) for (desc, ext) in zip(descs, exts)]
return '|'.join(wcParts) return '|'.join(wcParts)
...@@ -77,7 +79,7 @@ def isSupported(filename, allowedExts=None): ...@@ -77,7 +79,7 @@ def isSupported(filename, allowedExts=None):
if allowedExts is None: allowedExts = ALLOWED_EXTENSIONS if allowedExts is None: allowedExts = ALLOWED_EXTENSIONS
return any(map(lambda ext: filename.endswith(ext, allowedExts))) return any(map(lambda ext: filename.endswith(ext), allowedExts))
def removeExt(filename, allowedExts=None): def removeExt(filename, allowedExts=None):
...@@ -208,10 +210,8 @@ def loadImage(filename): ...@@ -208,10 +210,8 @@ def loadImage(filename):
unzipped = os.fdopen(unzipped) unzipped = os.fdopen(unzipped)
msg = '{} is a large file ({} MB) - decompressing ' \ msg = strings.messages['imageio.loadImage.decompress']
'to {}, to allow memory mapping...'.format(realFilename, msg = msg.format(realFilename, mbytes, filename)
mbytes,
filename)
if not haveGui: if not haveGui:
log.info(msg) log.info(msg)
...@@ -297,14 +297,11 @@ def saveImage(image, imageList=None, fromDir=None): ...@@ -297,14 +297,11 @@ def saveImage(image, imageList=None, fromDir=None):
saveLastDir = True saveLastDir = True
dlg = wx.FileDialog(app.GetTopWindow(), dlg = wx.FileDialog(app.GetTopWindow(),
message='Save image file', message=strings.titles['imageio.saveImage.dialog'],
defaultDir=fromDir, defaultDir=fromDir,
defaultFile=filename, defaultFile=filename,
wildcard=makeWildcard(),
style=wx.FD_SAVE) style=wx.FD_SAVE)
dlg.SetFilterIndex(ALLOWED_EXTENSIONS.index(DEFAULT_EXTENSION))
if dlg.ShowModal() != wx.ID_OK: return False if dlg.ShowModal() != wx.ID_OK: return False
if saveLastDir: saveImage.lastDir = lastDir if saveLastDir: saveImage.lastDir = lastDir
...@@ -312,37 +309,48 @@ def saveImage(image, imageList=None, fromDir=None): ...@@ -312,37 +309,48 @@ def saveImage(image, imageList=None, fromDir=None):
path = dlg.GetPath() path = dlg.GetPath()
nibImage = image.nibImage nibImage = image.nibImage
if not isSupported(path):
path = addExt(path, False)
# this is an image which has been # this is an image which has been
# loaded from a file, and ungzipped # loaded from a file, and ungzipped
# to a temporary location # to a temporary location
if image.tempFile is not None: try:
if image.tempFile is not None:
# if selected path is same as original path, # if selected path is same as original path,
# save to both temp file and to path # save to both temp file and to path
# else, if selected path is different from # else, if selected path is different from
# original path, save to temp file and to # original path, save to temp file and to
# new path, and update the path # new path, and update the path
# actually, the two behaviours just described # actually, the two behaviours just described
# are identical # are identical
pass pass
# TODO handle error
# TODO handle error # this is just a normal image
# which has been loaded from
# a file, or an in-memory image
else:
# this is just a normal image log.debug('Saving image ({}) to {}'.format(image, path))
# which has been loaded from
# a file, or an in-memory image
else:
log.debug('Saving image ({}) to {}'.format(image, path)) nib.save(nibImage, path)
image.imageFile = path
nib.save(nibImage, path)
image.imageFile = path except Exception as e:
image.saved = True msg = strings.messages['imageio.saveImage.error'].format(e.msg)
log.warn(msg)
wx.MessageDialog(app.GetTopWindow(),
message=msg,
style=wx.OK | wx.ICON_ERROR)
return
image.saved = True
def addImages(imageList, fromDir=None, addToEnd=True): def addImages(imageList, fromDir=None, addToEnd=True):
...@@ -389,7 +397,7 @@ def addImages(imageList, fromDir=None, addToEnd=True): ...@@ -389,7 +397,7 @@ def addImages(imageList, fromDir=None, addToEnd=True):
saveLastDir = True saveLastDir = True
dlg = wx.FileDialog(app.GetTopWindow(), dlg = wx.FileDialog(app.GetTopWindow(),
message='Open image file', message=strings.titles['imageio.addImages.dialog'],
defaultDir=fromDir, defaultDir=fromDir,
wildcard=makeWildcard(), wildcard=makeWildcard(),
style=wx.FD_OPEN | wx.FD_MULTIPLE) style=wx.FD_OPEN | wx.FD_MULTIPLE)
......
#!/usr/bin/env python
#
# strings.py -
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
from fsl.utils.typedict import TypeDict
import fsl.data.constants as constants
messages = TypeDict({
'imageio.saveImage.error' : 'An error occurred saving the file. '
'Details: {}',
'imageio.loadImage.decompress' : '{} is a large file ({} MB) - '
'decompressing to {}, to allow memory '
'mapping...',
})
titles = TypeDict({
'imageio.saveImage.dialog' : 'Save image file',
'imageio.addImages.dialog' : 'Open image files',
'OrthoPanel' : 'Ortho View',
'LightBoxPanel' : 'Lightbox View',
'TimeSeriesPanel' : 'Time series',
'SpacePanel' : 'Space inspector',
})
actions = TypeDict({
'OpenFileAction' : 'Add image file',
'OpenStandardAction' : 'Add standard',
'CopyImageAction' : 'Copy image',
'SaveImageAction' : 'Save image',
'LoadColourMapAction' : 'Load custom colour map',
'CanvasPanel.screenshot' : 'Take screenshot',
'CanvasPanel.toggleColourBar' : 'Show/hide colour bar',
'CanvasPanel.toggleImageList' : 'Show/hide image list',
'CanvasPanel.toggleDisplayProperties' : 'Show/hide display properties',
'CanvasPanel.toggleLocationPanel' : 'Show/hide location panel',
'CanvasPanel.toggleCanvasProperties' : 'Show/hide canvas properties',
'OrthoViewProfile.centreCursor' : 'Centre cursor',
'OrthoViewProfile.resetZoom' : 'Reset zoom',
'OrthoEditProfile.undo' : 'Undo',
'OrthoEditProfile.redo' : 'Redo',
'OrthoEditProfile.fillSelection' : 'Fill selected region',
'OrthoEditProfile.clearSelection' : 'Clear selection',
'OrthoEditProfile.createMaskFromSelection' : 'Create mask from '
'selected region',
'OrthoEditProfile.createROIFromSelection' : 'Create ROI from '
'selected region',
})
labels = TypeDict({
'LocationPanel.worldLabel' : 'World location (mm)',
'LocationPanel.voxelLabel' : 'Voxel location',
'LocationPanel.volumeLabel' : 'Volume',
'LocationPanel.spaceLabel' : 'Space',
'LocationPanel.outOfBounds' : 'Out of bounds',
})
properties = TypeDict({
'Profile.mode' : 'Mode',
'CanvasPanel.showCursor' : 'Show location cursor',
'CanvasPanel.syncLocation' : 'Sync location',
'CanvasPanel.syncImageOrder' : 'Sync image order',
'CanvasPanel.syncVolume' : 'Sync volume',
'CanvasPanel.profile' : 'Profile',
'CanvasPanel.zoom' : 'Zoom',
'CanvasPanel.colourBarLocation' : 'Colour bar location',
'CanvasPanel.colourBarLabelSide' : 'Colour bar label side',
'LightBoxPanel.zax' : 'Z axis',
'LightBoxPanel.highlightSlice' : 'Highlight slice',
'LightBoxPanel.showGridLines' : 'Show grid lines',
'LightBoxPanel.sliceSpacing' : 'Slice spacing',
'LightBoxPanel.zrange' : 'Z range',
'OrthoPanel.showXCanvas' : 'Show X canvas',
'OrthoPanel.showYCanvas' : 'Show Y canvas',
'OrthoPanel.showZCanvas' : 'Show Z canvas',
'OrthoPanel.showLabels' : 'Show labels',
'OrthoPanel.layout' : 'Layout',
'OrthoPanel.xzoom' : 'X zoom',
'OrthoPanel.yzoom' : 'Y zoom',
'OrthoPanel.zzoom' : 'Z zoom',
'OrthoEditProfile.selectionMode' : 'Selection mode',
'OrthoEditProfile.selectionSize' : 'Selection size',
'OrthoEditProfile.selectionIs3D' : '3D selection',
'OrthoEditProfile.fillValue' : 'Fill value',
'OrthoEditProfile.intensityThres' : 'Intensity threshold',
'OrthoEditProfile.localFill' : 'Only select adjacent voxels',
'OrthoEditProfile.searchRadius' : 'Limit search to radius (mm)',
'ImageDisplay.name' : 'Image name',
'ImageDisplay.enabled' : 'Enabled',
'ImageDisplay.displayRange' : 'Display range',
'ImageDisplay.alpha' : 'Opacity',
'ImageDisplay.clipLow' : 'Low clipping',
'ImageDisplay.clipHigh' : 'High clipping',
'ImageDisplay.interpolation' : 'Interpolation',
'ImageDisplay.resolution' : 'Resolution',
'ImageDisplay.volume' : 'Volume',
'ImageDisplay.syncVolume' : 'Synchronise volume',
'ImageDisplay.transform' : 'Image transform',
'ImageDisplay.imageType' : 'Image data type',
'ImageDisplay.cmap' : 'Colour map',
})
profiles = TypeDict({
'CanvasPanel.view' : 'View',
'OrthoPanel.edit' : 'Edit',
})
modes = TypeDict({
('OrthoViewProfile', 'nav') : 'Navigate',
('OrthoViewProfile', 'pan') : 'Pan',
('OrthoViewProfile', 'zoom') : 'Zoom',
('OrthoEditProfile', 'nav') : 'Navigate',
('OrthoEditProfile', 'pan') : 'Pan',
('OrthoEditProfile', 'zoom') : 'Zoom',
('OrthoEditProfile', 'sel') : 'Select',
('OrthoEditProfile', 'desel') : 'Deselect',
('OrthoEditProfile', 'selint') : 'Select by intensity',
})
choices = TypeDict({
'CanvasPanel.colourBarLocation.top' : 'Top',
'CanvasPanel.colourBarLocation.bottom' : 'Bottom',
'CanvasPanel.colourBarLocation.left' : 'Left',
'CanvasPanel.colourBarLocation.right' : 'Right',
'ColourBarCanvas.orientation.horizontal' : 'Horizontal',
'ColourBarCanvas.orientation.vertical' : 'Vertical',
'ColourBarCanvas.labelSide.top-left' : 'Top / Left',
'ColourBarCanvas.labelSide.bottom-right' : 'Bottom / Right',
'ImageDisplay.displayRange.min' : 'Min.',
'ImageDisplay.displayRange.max' : 'Max.',
'ImageDisplay.transform.affine' : 'Use qform/sform transformation matrix',
'ImageDisplay.transform.pixdim' : 'Use pixdims only',
'ImageDisplay.transform.id' : 'Do not use qform/sform or pixdims',
'ImageDisplay.interpolation.none' : 'No interpolation',
'ImageDisplay.interpolation.linear' : 'Linear interpolation',
'ImageDisplay.interpolation.spline' : 'Spline interpolation',
})
anatomy = TypeDict({
('Image', 'lowlong', constants.ORIENT_A2P) : 'Anterior',
('Image', 'lowlong', constants.ORIENT_P2A) : 'Posterior',
('Image', 'lowlong', constants.ORIENT_L2R) : 'Left',
('Image', 'lowlong', constants.ORIENT_R2L) : 'Right',
('Image', 'lowlong', constants.ORIENT_I2S) : 'Inferior',
('Image', 'lowlong', constants.ORIENT_S2I) : 'Superior',
('Image', 'lowlong', constants.ORIENT_UNKNOWN) : 'Unknown',
('Image', 'highlong', constants.ORIENT_A2P) : 'Posterior',
('Image', 'highlong', constants.ORIENT_P2A) : 'Anterior',
('Image', 'highlong', constants.ORIENT_L2R) : 'Right',
('Image', 'highlong', constants.ORIENT_R2L) : 'Left',
('Image', 'highlong', constants.ORIENT_I2S) : 'Superior',
('Image', 'highlong', constants.ORIENT_S2I) : 'Inferior',
('Image', 'highlong', constants.ORIENT_UNKNOWN) : 'Unknown',
('Image', 'lowshort', constants.ORIENT_A2P) : 'A',
('Image', 'lowshort', constants.ORIENT_P2A) : 'P',
('Image', 'lowshort', constants.ORIENT_L2R) : 'L',
('Image', 'lowshort', constants.ORIENT_R2L) : 'R',
('Image', 'lowshort', constants.ORIENT_I2S) : 'I',
('Image', 'lowshort', constants.ORIENT_S2I) : 'S',
('Image', 'lowshort', constants.ORIENT_UNKNOWN) : '?',
('Image', 'highshort', constants.ORIENT_A2P) : 'P',
('Image', 'highshort', constants.ORIENT_P2A) : 'A',
('Image', 'highshort', constants.ORIENT_L2R) : 'R',
('Image', 'highshort', constants.ORIENT_R2L) : 'L',
('Image', 'highshort', constants.ORIENT_I2S) : 'S',
('Image', 'highshort', constants.ORIENT_S2I) : 'I',
('Image', 'highshort', constants.ORIENT_UNKNOWN) : '?',
('Image', 'space', constants.NIFTI_XFORM_UNKNOWN) : 'Unknown',
('Image', 'space', constants.NIFTI_XFORM_SCANNER_ANAT) : 'Scanner anatomical',
('Image', 'space', constants.NIFTI_XFORM_ALIGNED_ANAT) : 'Aligned anatomical',
('Image', 'space', constants.NIFTI_XFORM_TALAIRACH) : 'Talairach',
('Image', 'space', constants.NIFTI_XFORM_MNI_152) : 'MNI152',
})
...@@ -24,7 +24,7 @@ import numpy as np ...@@ -24,7 +24,7 @@ import numpy as np
import props import props
import fsl.utils.transform as transform import fsl.utils.transform as transform
import fsl.fslview.strings as strings import fsl.data.strings as strings
import fsl.fslview.panel as fslpanel import fsl.fslview.panel as fslpanel
import imageselectpanel as imageselect import imageselectpanel as imageselect
...@@ -335,7 +335,7 @@ class LocationPanel(fslpanel.FSLViewPanel): ...@@ -335,7 +335,7 @@ class LocationPanel(fslpanel.FSLViewPanel):
# Update the label which # Update the label which
# displays the image space # displays the image space
spaceLabel = strings.labels['Image', 'space', image.getXFormCode()] spaceLabel = strings.anatomy['Image', 'space', image.getXFormCode()]
spaceLabel = '{} {}'.format(spaceLabel, spaceLabel = '{} {}'.format(spaceLabel,
strings.labels[self, 'spaceLabel']) strings.labels[self, 'spaceLabel'])
self._spaceLabel.SetLabel(spaceLabel) self._spaceLabel.SetLabel(spaceLabel)
......
...@@ -6,18 +6,16 @@ ...@@ -6,18 +6,16 @@
# #
import sys import sys
import collections import numpy as np
from collections import OrderedDict
import numpy as np
import props import props
import fsl.data.image as fslimage import fsl.data.image as fslimage
import fsl.utils.transform as transform import fsl.utils.transform as transform
import fsl.fslview.strings as strings import fsl.data.strings as strings
import fsl.fslview.colourmaps as fslcm import fsl.fslview.colourmaps as fslcm
class ImageDisplay(props.SyncableHasProperties): class ImageDisplay(props.SyncableHasProperties):
"""A class which describes how an :class:`~fsl.data.image.Image` should """A class which describes how an :class:`~fsl.data.image.Image` should
be displayed. be displayed.
...@@ -39,7 +37,8 @@ class ImageDisplay(props.SyncableHasProperties): ...@@ -39,7 +37,8 @@ class ImageDisplay(props.SyncableHasProperties):
displayRange = props.Bounds( displayRange = props.Bounds(
ndims=1, ndims=1,
editLimits=True, editLimits=True,
labels=strings.labels['ImageDisplay', 'displayRange', 'labels']) labels=[strings.choices['ImageDisplay.displayRange.min'],
strings.choices['ImageDisplay.displayRange.max']])
"""Image values which map to the minimum and maximum colour map colours.""" """Image values which map to the minimum and maximum colour map colours."""
...@@ -76,7 +75,9 @@ class ImageDisplay(props.SyncableHasProperties): ...@@ -76,7 +75,9 @@ class ImageDisplay(props.SyncableHasProperties):
transform = props.Choice( transform = props.Choice(
('affine', 'pixdim', 'id'), ('affine', 'pixdim', 'id'),
labels=strings.labels['ImageDisplay', 'transform', 'labels'], labels=[strings.choices['ImageDisplay.transform.affine'],
strings.choices['ImageDisplay.transform.pixdim'],
strings.choices['ImageDisplay.transform.id']],
default='pixdim') default='pixdim')
"""This property defines how the image should be transformd into the display """This property defines how the image should be transformd into the display
coordinate system. coordinate system.
...@@ -95,7 +96,9 @@ class ImageDisplay(props.SyncableHasProperties): ...@@ -95,7 +96,9 @@ class ImageDisplay(props.SyncableHasProperties):
interpolation = props.Choice( interpolation = props.Choice(
('none', 'linear', 'spline'), ('none', 'linear', 'spline'),
labels=strings.labels['ImageDisplay', 'interpolation', 'labels']) labels=[strings.choices['ImageDisplay.interpolation.none'],
strings.choices['ImageDisplay.interpolation.linear'],
strings.choices['ImageDisplay.interpolation.spline']])
"""How the value shown at a real world location is derived from the """How the value shown at a real world location is derived from the
corresponding voxel value(s). 'No interpolation' is equivalent to nearest corresponding voxel value(s). 'No interpolation' is equivalent to nearest
neighbour interpolation. neighbour interpolation.
......
...@@ -49,9 +49,10 @@ log = logging.getLogger(__name__) ...@@ -49,9 +49,10 @@ log = logging.getLogger(__name__)
import wx import wx
import wx.aui as aui import wx.aui as aui
import fsl.data.strings as strings
import views import views
import actions import actions
import strings
import displaycontext import displaycontext
...@@ -110,7 +111,7 @@ class FSLViewFrame(wx.Frame): ...@@ -110,7 +111,7 @@ class FSLViewFrame(wx.Frame):
allowing the user to configure the view. allowing the user to configure the view.
""" """
title = strings.labels[panelCls] title = strings.titles[panelCls]
childDC = displaycontext.DisplayContext(self._imageList, childDC = displaycontext.DisplayContext(self._imageList,
self._displayCtx) self._displayCtx)
panel = panelCls(self._centrePane, panel = panelCls(self._centrePane,
...@@ -240,14 +241,14 @@ class FSLViewFrame(wx.Frame): ...@@ -240,14 +241,14 @@ class FSLViewFrame(wx.Frame):
actionz = actions .listGlobalActions() actionz = actions .listGlobalActions()
for action in actionz: for action in actionz:
menuItem = fileMenu.Append(wx.ID_ANY, strings.labels[action]) menuItem = fileMenu.Append(wx.ID_ANY, strings.actions[action])
actionObj = action(self._imageList, self._displayCtx) actionObj = action(self._imageList, self._displayCtx)
actionObj.bindToWidget(self, wx.EVT_MENU, menuItem) actionObj.bindToWidget(self, wx.EVT_MENU, menuItem)
for viewPanel in viewPanels: for viewPanel in viewPanels:
viewAction = viewMenu.Append(wx.ID_ANY, strings.labels[viewPanel]) viewAction = viewMenu.Append(wx.ID_ANY, strings.titles[viewPanel])
self.Bind(wx.EVT_MENU, self.Bind(wx.EVT_MENU,
lambda ev, vp=viewPanel: self.addViewPanel(vp), lambda ev, vp=viewPanel: self.addViewPanel(vp),
viewAction) viewAction)
...@@ -23,7 +23,7 @@ import numpy as np ...@@ -23,7 +23,7 @@ import numpy as np
import props import props
import fsl.utils.colourbarbitmap as cbarbmp import fsl.utils.colourbarbitmap as cbarbmp
import fsl.fslview.strings as strings import fsl.data.strings as strings
class ColourBarCanvas(props.HasProperties): class ColourBarCanvas(props.HasProperties):
...@@ -44,13 +44,15 @@ class ColourBarCanvas(props.HasProperties): ...@@ -44,13 +44,15 @@ class ColourBarCanvas(props.HasProperties):
orientation = props.Choice( orientation = props.Choice(
('horizontal', 'vertical'), ('horizontal', 'vertical'),
labels=strings.labels['ColourBarCanvas', 'orientation', 'labels']) labels=[strings.choices['ColourBarCanvas.orientation.horizontal'],
strings.choices['ColourBarCanvas.orientation.vertical']])
"""Whether the colour bar should be vertical or horizontal. """ """Whether the colour bar should be vertical or horizontal. """
labelSide = props.Choice( labelSide = props.Choice(
('top-left', 'bottom-right'), ('top-left', 'bottom-right'),
labels=strings.labels['ColourBarCanvas', 'labelSide', 'labels']) labels=[strings.choices['ColourBarCanvas.labelSide.top-left'],
strings.choices['ColourBarCanvas.labelSide.bottom-right']])
"""Whether the colour bar labels should be on the top/left, or bottom/right """Whether the colour bar labels should be on the top/left, or bottom/right
of the colour bar (depending upon whether the colour bar orientation is of the colour bar (depending upon whether the colour bar orientation is
horizontal/vertical). horizontal/vertical).
......
...@@ -12,7 +12,7 @@ import props ...@@ -12,7 +12,7 @@ import props
import fsl.utils.typedict as td import fsl.utils.typedict as td
import fsl.fslview.strings as strings import fsl.data.strings as strings
from fsl.fslview.profiles.orthoviewprofile import OrthoViewProfile from fsl.fslview.profiles.orthoviewprofile import OrthoViewProfile
from fsl.fslview.profiles.orthoeditprofile import OrthoEditProfile from fsl.fslview.profiles.orthoeditprofile import OrthoEditProfile
...@@ -23,7 +23,7 @@ from fsl.fslview.displaycontext import ImageDisplay ...@@ -23,7 +23,7 @@ from fsl.fslview.displaycontext import ImageDisplay
def widget(name, labelCls, *args, **kwargs): def widget(name, labelCls, *args, **kwargs):
return props.Widget(name, return props.Widget(name,
label=strings.labels[labelCls, name], label=strings.properties[labelCls, name],
*args, *args,
**kwargs) **kwargs)
...@@ -32,7 +32,7 @@ def actionButton(name, labelCls, *args, **kwargs): ...@@ -32,7 +32,7 @@ def actionButton(name, labelCls, *args, **kwargs):
def callback(i, *a): def callback(i, *a):
i.run(name) i.run(name)
return props.Button(name, return props.Button(name,
text=strings.labels[labelCls, name], text=strings.actions[labelCls, name],
callback=callback) callback=callback)
......
...@@ -31,7 +31,7 @@ import inspect ...@@ -31,7 +31,7 @@ import inspect
import wx import wx
import props import props
import fsl.fslview.strings as strings import fsl.data.strings as strings
import fsl.fslview.actions as actions import fsl.fslview.actions as actions
...@@ -168,7 +168,7 @@ class Profile(actions.ActionProvider): ...@@ -168,7 +168,7 @@ class Profile(actions.ActionProvider):
modeProp = self.getProp('mode') modeProp = self.getProp('mode')
for mode in modes: for mode in modes:
modeProp.addChoice(mode, strings.labels[self, mode], self) modeProp.addChoice(mode, strings.modes[self, mode], self)
if len(modes) > 0: if len(modes) > 0:
self.mode = modes[0] self.mode = modes[0]
...@@ -542,7 +542,6 @@ class ProfileManager(object): ...@@ -542,7 +542,6 @@ class ProfileManager(object):
displayed. displayed.
""" """
import fsl.fslview.profilemap as profilemap import fsl.fslview.profilemap as profilemap
import fsl.fslview.strings as strings
self._canvasPanel = canvasPanel self._canvasPanel = canvasPanel
self._canvasCls = canvasPanel.__class__ self._canvasCls = canvasPanel.__class__
...@@ -556,7 +555,7 @@ class ProfileManager(object): ...@@ -556,7 +555,7 @@ class ProfileManager(object):
for profile in profilez: for profile in profilez:
profileProp.addChoice( profileProp.addChoice(
profile, profile,
strings.labels[canvasPanel, 'profile', profile], strings.profiles[canvasPanel, profile],
canvasPanel) canvasPanel)
canvasPanel.profile = profilez[0] canvasPanel.profile = profilez[0]
......
#!/usr/bin/env python
#
# strings.py - Labels used throughout various parts of FSLView.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module is a home for labels and tooltips used throughout FSLView.
- labels
- tooltips
"""
import logging
log = logging.getLogger(__name__)
import fsl.utils.typedict as td
import fsl.data.image as fslimage
labels = td.TypeDict({
'OrthoPanel' : 'Ortho View',
'LightBoxPanel' : 'Lightbox View',
'TimeSeriesPanel' : 'Time series',
'SpacePanel' : 'Space inspector',
'OpenFileAction' : 'Add image file',
'OpenStandardAction' : 'Add standard',
'CopyImageAction' : 'Copy image',
'SaveImageAction' : 'Save image',
'LoadColourMapAction' : 'Load custom colour map',
('Profile', 'mode') : 'Mode',
('CanvasPanel', 'profile', 'view') : 'View',
('OrthoPanel', 'profile', 'edit') : 'Edit',
('CanvasPanel', 'screenshot') : 'Take screenshot',
('CanvasPanel', 'toggleColourBar') : 'Show/hide colour bar',
('CanvasPanel', 'toggleImageList') : 'Show/hide image list',
('CanvasPanel', 'toggleDisplayProperties') : 'Show/hide display properties',
('CanvasPanel', 'toggleLocationPanel') : 'Show/hide location panel',
('CanvasPanel', 'toggleCanvasProperties') : 'Show/hide canvas properties',
('CanvasPanel', 'colourBarLocation', 'labels') : ('Top', 'Bottom', 'Left', 'Right'),
('ColourBarCanvas', 'orientation', 'labels') : ('Horizontal', 'Vertical'),
('ColourBarCanvas', 'labelSide', 'labels') : ('Top / Left', 'Bottom / Right'),
('CanvasPanel', 'zax') : 'Z axis',
('CanvasPanel', 'showCursor') : 'Show location cursor',
('CanvasPanel', 'syncLocation') : 'Sync location',
('CanvasPanel', 'syncImageOrder') : 'Sync image order',
('CanvasPanel', 'syncVolume') : 'Sync volume',
('CanvasPanel', 'profile') : 'Profile',
('CanvasPanel', 'zoom') : 'Zoom',
('CanvasPanel', 'colourBarLocation') : 'Colour bar location',
('CanvasPanel', 'colourBarLabelSide') : 'Colour bar label side',
('LightBoxPanel', 'highlightSlice') : 'Highlight slice',
('LightBoxPanel', 'showGridLines') : 'Show grid lines',
('LightBoxPanel', 'sliceSpacing') : 'Slice spacing',
('LightBoxPanel', 'zrange') : 'Z range',
('OrthoPanel', 'showXCanvas') : 'Show X canvas',
('OrthoPanel', 'showYCanvas') : 'Show Y canvas',
('OrthoPanel', 'showZCanvas') : 'Show Z canvas',
('OrthoPanel', 'showLabels') : 'Show labels',
('OrthoPanel', 'layout') : 'Layout',
('OrthoPanel', 'xzoom') : 'X zoom',
('OrthoPanel', 'yzoom') : 'Y zoom',
('OrthoPanel', 'zzoom') : 'Z zoom',
('Image', 'lowlong', fslimage.ORIENT_A2P) : 'Anterior',
('Image', 'lowlong', fslimage.ORIENT_P2A) : 'Posterior',
('Image', 'lowlong', fslimage.ORIENT_L2R) : 'Left',
('Image', 'lowlong', fslimage.ORIENT_R2L) : 'Right',
('Image', 'lowlong', fslimage.ORIENT_I2S) : 'Inferior',
('Image', 'lowlong', fslimage.ORIENT_S2I) : 'Superior',
('Image', 'lowlong', fslimage.ORIENT_UNKNOWN) : 'Unknown',
('Image', 'highlong', fslimage.ORIENT_A2P) : 'Posterior',
('Image', 'highlong', fslimage.ORIENT_P2A) : 'Anterior',
('Image', 'highlong', fslimage.ORIENT_L2R) : 'Right',
('Image', 'highlong', fslimage.ORIENT_R2L) : 'Left',
('Image', 'highlong', fslimage.ORIENT_I2S) : 'Superior',
('Image', 'highlong', fslimage.ORIENT_S2I) : 'Inferior',
('Image', 'highlong', fslimage.ORIENT_UNKNOWN) : 'Unknown',
('Image', 'lowshort', fslimage.ORIENT_A2P) : 'A',
('Image', 'lowshort', fslimage.ORIENT_P2A) : 'P',
('Image', 'lowshort', fslimage.ORIENT_L2R) : 'L',
('Image', 'lowshort', fslimage.ORIENT_R2L) : 'R',
('Image', 'lowshort', fslimage.ORIENT_I2S) : 'I',
('Image', 'lowshort', fslimage.ORIENT_S2I) : 'S',
('Image', 'lowshort', fslimage.ORIENT_UNKNOWN) : '?',
('Image', 'highshort', fslimage.ORIENT_A2P) : 'P',
('Image', 'highshort', fslimage.ORIENT_P2A) : 'A',
('Image', 'highshort', fslimage.ORIENT_L2R) : 'R',
('Image', 'highshort', fslimage.ORIENT_R2L) : 'L',
('Image', 'highshort', fslimage.ORIENT_I2S) : 'S',
('Image', 'highshort', fslimage.ORIENT_S2I) : 'I',
('Image', 'highshort', fslimage.ORIENT_UNKNOWN) : '?',
('Image', 'space', fslimage.NIFTI_XFORM_UNKNOWN) : 'Unknown',
('Image', 'space', fslimage.NIFTI_XFORM_SCANNER_ANAT) : 'Scanner anatomical',
('Image', 'space', fslimage.NIFTI_XFORM_ALIGNED_ANAT) : 'Aligned anatomical',
('Image', 'space', fslimage.NIFTI_XFORM_TALAIRACH) : 'Talairach',
('Image', 'space', fslimage.NIFTI_XFORM_MNI_152) : 'MNI152',
('LocationPanel', 'outOfBounds') : 'Out of bounds',
('LocationPanel', 'spaceLabel') : 'space',
('LocationPanel', 'worldLabel') : 'World location (mm)',
('LocationPanel', 'voxelLabel') : 'Voxel coordinates',
('LocationPanel', 'volumeLabel') : 'Volume (index)' ,
('OrthoViewProfile', 'nav') : 'Navigate',
('OrthoViewProfile', 'pan') : 'Pan',
('OrthoViewProfile', 'zoom') : 'Zoom',
('OrthoEditProfile', 'nav') : 'Navigate',
('OrthoEditProfile', 'pan') : 'Pan',
('OrthoEditProfile', 'zoom') : 'Zoom',
('OrthoEditProfile', 'sel') : 'Select',
('OrthoEditProfile', 'desel') : 'Deselect',
('OrthoEditProfile', 'selint') : 'Select by intensity',
('OrthoViewProfile', 'centreCursor') : 'Centre cursor',
('OrthoViewProfile', 'resetZoom') : 'Reset zoom',
('OrthoEditProfile', 'undo') : 'Undo',
('OrthoEditProfile', 'redo') : 'Redo',
('OrthoEditProfile', 'fillSelection') : 'Fill selected region',
('OrthoEditProfile', 'clearSelection') : 'Clear selection',
('OrthoEditProfile', 'createMaskFromSelection') : 'Create mask from selected region',
('OrthoEditProfile', 'createROIFromSelection') : 'Create ROI from selected region',
('OrthoEditProfile', 'selectionMode') : 'Selection mode',
('OrthoEditProfile', 'selectionSize') : 'Selection size',
('OrthoEditProfile', 'selectionIs3D') : '3D selection',
('OrthoEditProfile', 'fillValue') : 'Fill value',
('OrthoEditProfile', 'intensityThres') : 'Intensity threshold',
('OrthoEditProfile', 'localFill') : 'Only select adjacent voxels',
('OrthoEditProfile', 'searchRadius') : 'Limit search to radius (mm)',
('ImageDisplay', 'name') : 'Image name',
('ImageDisplay', 'enabled') : 'Enabled',
('ImageDisplay', 'displayRange') : 'Display range',
('ImageDisplay', 'alpha') : 'Opacity',
('ImageDisplay', 'clipLow') : 'Low clipping',
('ImageDisplay', 'clipHigh') : 'High clipping',
('ImageDisplay', 'interpolation') : 'Interpolation',
('ImageDisplay', 'resolution') : 'Resolution',
('ImageDisplay', 'volume') : 'Volume',
('ImageDisplay', 'syncVolume') : 'Synchronise volume',
('ImageDisplay', 'transform') : 'Image transform',
('ImageDisplay', 'imageType') : 'Image data type',
('ImageDisplay', 'cmap') : 'Colour map',
('ImageDisplay', 'displayRange', 'labels') : ['Min.', 'Max.'],
('ImageDisplay', 'transform', 'labels') : ['Use qform/sform transformation matrix',
'Use pixdims only',
'Do not use qform/sform or pixdims'],
('ImageDisplay', 'interpolation', 'labels') : ['No interpolation',
'Linear interpolation',
'Spline interpolation'],
})
...@@ -23,8 +23,8 @@ import wx ...@@ -23,8 +23,8 @@ import wx
import props import props
import fsl.data.strings as strings
import fsl.fslview.panel as fslpanel import fsl.fslview.panel as fslpanel
import fsl.fslview.strings as strings
import fsl.fslview.profiles as profiles import fsl.fslview.profiles as profiles
import fsl.fslview.displaycontext as displayctx import fsl.fslview.displaycontext as displayctx
import fsl.fslview.controls.imagelistpanel as imagelistpanel import fsl.fslview.controls.imagelistpanel as imagelistpanel
...@@ -163,7 +163,12 @@ class CanvasPanel(fslpanel.FSLViewPanel): ...@@ -163,7 +163,12 @@ class CanvasPanel(fslpanel.FSLViewPanel):
colourBarLocation = props.Choice( colourBarLocation = props.Choice(
('top', 'bottom', 'left', 'right'), ('top', 'bottom', 'left', 'right'),
labels=strings.labels['CanvasPanel', 'colourBarLocation', 'labels']) labels=[strings.choices['CanvasPanel.colourBarLocation.top'],
strings.choices['CanvasPanel.colourBarLocation.bottom'],
strings.choices['CanvasPanel.colourBarLocation.left'],
strings.choices['CanvasPanel.colourBarLocation.right']])
colourBarLabelSide = colourbarpanel.ColourBarPanel.labelSide colourBarLabelSide = colourbarpanel.ColourBarPanel.labelSide
......
...@@ -21,9 +21,9 @@ import copy ...@@ -21,9 +21,9 @@ import copy
import wx import wx
import props import props
import fsl.data.image as fslimage import fsl.data.strings as strings
import fsl.data.constants as constants
import fsl.utils.layout as fsllayout import fsl.utils.layout as fsllayout
import fsl.fslview.strings as strings
import fsl.fslview.gl as fslgl import fsl.fslview.gl as fslgl
import fsl.fslview.gl.wxglslicecanvas as slicecanvas import fsl.fslview.gl.wxglslicecanvas as slicecanvas
import canvaspanel import canvaspanel
...@@ -358,15 +358,15 @@ class OrthoPanel(canvaspanel.CanvasPanel): ...@@ -358,15 +358,15 @@ class OrthoPanel(canvaspanel.CanvasPanel):
yorient = image.getWorldOrientation(1) yorient = image.getWorldOrientation(1)
zorient = image.getWorldOrientation(2) zorient = image.getWorldOrientation(2)
if fslimage.ORIENT_UNKNOWN in (xorient, yorient, zorient): if constants.ORIENT_UNKNOWN in (xorient, yorient, zorient):
colour = 'red' colour = 'red'
xlo = strings.labels['Image', 'lowshort', xorient] xlo = strings.anatomy['Image', 'lowshort', xorient]
ylo = strings.labels['Image', 'lowshort', yorient] ylo = strings.anatomy['Image', 'lowshort', yorient]
zlo = strings.labels['Image', 'lowshort', zorient] zlo = strings.anatomy['Image', 'lowshort', zorient]
xhi = strings.labels['Image', 'highshort', xorient] xhi = strings.anatomy['Image', 'highshort', xorient]
yhi = strings.labels['Image', 'highshort', yorient] yhi = strings.anatomy['Image', 'highshort', yorient]
zhi = strings.labels['Image', 'highshort', zorient] zhi = strings.anatomy['Image', 'highshort', zorient]
for lbl in allLabels: for lbl in allLabels:
lbl.SetForegroundColour(colour) lbl.SetForegroundColour(colour)
......
...@@ -16,8 +16,9 @@ import wx ...@@ -16,8 +16,9 @@ import wx
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as Canvas from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as Canvas
from mpl_toolkits.mplot3d import Axes3D from mpl_toolkits.mplot3d import Axes3D
import fsl.fslview.panel as fslpanel import fsl.data.strings as strings
import fsl.utils.transform as transform import fsl.utils.transform as transform
import fsl.fslview.panel as fslpanel
class SpacePanel(fslpanel.FSLViewPanel): class SpacePanel(fslpanel.FSLViewPanel):
...@@ -125,9 +126,6 @@ class SpacePanel(fslpanel.FSLViewPanel): ...@@ -125,9 +126,6 @@ class SpacePanel(fslpanel.FSLViewPanel):
def _plotImageLabels(self): def _plotImageLabels(self):
# Imported here to avoid circular import issues
import fsl.fslview.strings as strings
image = self._displayCtx.getSelectedImage() image = self._displayCtx.getSelectedImage()
display = self._displayCtx.getDisplayProperties(image) display = self._displayCtx.getDisplayProperties(image)
...@@ -142,8 +140,8 @@ class SpacePanel(fslpanel.FSLViewPanel): ...@@ -142,8 +140,8 @@ class SpacePanel(fslpanel.FSLViewPanel):
orient = image.getVoxelOrientation(ax) orient = image.getVoxelOrientation(ax)
lblLo = strings.imageAxisLowShortLabels[ orient] lblLo = strings.anatomy['Image', 'lowshort', orient]
lblHi = strings.imageAxisHighShortLabels[orient] lblHi = strings.anatomy['Image', 'highshort', orient]
wldSpan = transform.transform(voxSpan, display.voxToDisplayMat) wldSpan = transform.transform(voxSpan, display.voxToDisplayMat)
......
...@@ -29,7 +29,7 @@ import fslview_parseargs ...@@ -29,7 +29,7 @@ import fslview_parseargs
import fsl.utils.layout as fsllayout import fsl.utils.layout as fsllayout
import fsl.utils.colourbarbitmap as cbarbitmap import fsl.utils.colourbarbitmap as cbarbitmap
import fsl.utils.textbitmap as textbitmap import fsl.utils.textbitmap as textbitmap
import fsl.fslview.strings as strings import fsl.data.strings as strings
import fsl.data.image as fslimage import fsl.data.image as fslimage
...@@ -75,12 +75,12 @@ def buildLabelBitmaps(imageList, ...@@ -75,12 +75,12 @@ def buildLabelBitmaps(imageList,
if fslimage.ORIENT_UNKNOWN in [xorient, yorient, zorient]: if fslimage.ORIENT_UNKNOWN in [xorient, yorient, zorient]:
fgColour = 'red' fgColour = 'red'
xlo = strings.imageAxisLowShortLabels[ xorient] xlo = strings.anatomy['Image', 'lowshort', xorient]
ylo = strings.imageAxisLowShortLabels[ yorient] ylo = strings.anatomy['Image', 'lowshort', yorient]
zlo = strings.imageAxisLowShortLabels[ zorient] zlo = strings.anatomy['Image', 'lowshort', zorient]
xhi = strings.imageAxisHighShortLabels[xorient] xhi = strings.anatomy['Image', 'highshort', xorient]
yhi = strings.imageAxisHighShortLabels[yorient] yhi = strings.anatomy['Image', 'highshort', yorient]
zhi = strings.imageAxisHighShortLabels[zorient] zhi = strings.anatomy['Image', 'highshort', zorient]
loLabels = [xlo, ylo, zlo] loLabels = [xlo, ylo, zlo]
hiLabels = [xhi, yhi, zhi] hiLabels = [xhi, yhi, zhi]
......
...@@ -21,13 +21,28 @@ class TypeDict(object): ...@@ -21,13 +21,28 @@ class TypeDict(object):
""" """
def __init__(self, initial=None): def __init__(self, initial=None):
self._dict = dict(initial)
if initial is None:
initial = {}
self.__dict = {}
for k, v in initial.items():
self[k] = v
def __setitem__(self, key, value): def __setitem__(self, key, value):
self._dict[key] = value self.__dict[self.__tokenifyKey(key)] = value
def __tokenifyKey(self, key):
if isinstance(key, basestring) and '.' in key:
return tuple(key.split('.'))
return key
def get(self, key, default): def get(self, key, default):
try: return self.__getitem__(key) try: return self.__getitem__(key)
except KeyError: return default except KeyError: return default
...@@ -36,6 +51,7 @@ class TypeDict(object): ...@@ -36,6 +51,7 @@ class TypeDict(object):
def __getitem__(self, key): def __getitem__(self, key):
origKey = key origKey = key
key = self.__tokenifyKey(key)
bases = [] bases = []
# Make the code a bit easier by # Make the code a bit easier by
...@@ -71,7 +87,7 @@ class TypeDict(object): ...@@ -71,7 +87,7 @@ class TypeDict(object):
if len(key) == 1: lKey = key[0] if len(key) == 1: lKey = key[0]
else: lKey = tuple(key) else: lKey = tuple(key)
val = self._dict.get(lKey, None) val = self.__dict.get(lKey, None)
if val is not None: if val is not None:
return val return val
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment