Source code for fsleyes.actions.saveoverlay

#
# saveoverlay.py - Save the currently selected overlay.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`SaveOverlayAction`, which allows the user
to save the currently selected overlay. A couple of standalone functions are
defined in this module, which do the real work:

.. autosummary::
   :nosignatures:

   saveOverlay
   doSave
   checkOverlaySaveState
"""


import logging

import                                 os
import os.path                      as op

import fsl.utils.settings           as fslsettings
import fsl.data.image               as fslimage
import fsleyes_widgets.utils.status as status
import fsleyes.strings              as strings
from . import                          base


log = logging.getLogger(__name__)


[docs]class SaveOverlayAction(base.Action): """The ``SaveOverlayAction`` allows the user to save the currently selected overlay, if it has been edited, or only exists in memory. """
[docs] def __init__(self, overlayList, displayCtx, frame): """Create a ``SaveOverlayAction``. :arg overlayList: The :class:`.OverlayList`. :arg displayCtx: The :class:`.DisplayContext`. :arg frame: The :class:`.FSLeyesFrame`. """ base.Action.__init__(self, overlayList, displayCtx, self.__saveOverlay) self.__name = '{}_{}'.format(type(self).__name__, id(self)) self.__registered = None displayCtx .addListener('selectedOverlay', self.__name, self.__selectedOverlayChanged) overlayList.addListener('overlays', self.__name, self.__selectedOverlayChanged) self.__selectedOverlayChanged()
[docs] def destroy(self): """Removes listeners from the :class:`.DisplayContext` and :class:`.OverlayList`, and calls :meth:`.Action.destroy`. """ self.displayCtx .removeListener('selectedOverlay', self.__name) self.overlayList.removeListener('overlays', self.__name) if self.__registered is not None: self.__registered.deregister(self.__name, 'saveState') self.__registered = None base.Action.destroy(self)
def __selectedOverlayChanged(self, *a): """Called when the selected overlay, or overlay list changes. If the overlay is a :class:`.Image`, and it has unsaved changes, this action is enabled; otherwise it is disabled. """ overlay = self.displayCtx.getSelectedOverlay() # TODO Support for other overlay types self.enabled = ((overlay is not None) and isinstance(overlay, fslimage.Image) and (not overlay.saveState)) if self.__registered is not None: self.__registered.deregister(self.__name, 'saveState') self.__registered = None # Register a listener on the saved property # of the currently selected image, so we can # enable the save action when the image # becomes 'unsaved', and vice versa. if self.enabled: self.__registered = overlay overlay.register(self.__name, self.__overlaySaveStateChanged, 'saveState') def __overlaySaveStateChanged(self, *a): """Called when the :attr:`.Image.saved` property of the currently selected overlay changes. Enables/disables this ``SaveOverlayAction`` accordingly. This is only applicable if the current overlay is a :class:`.Image` - see the :meth:`__selectedOverlayChanged` method. """ overlay = self.__registered self.enabled = (overlay is not None) and (not overlay.saveState) def __saveOverlay(self): """Called when this :class:`.Action` is executed. Calls :func:`saveOverlay` with the currently selected overlay. """ overlay = self.__registered if overlay is not None: display = self.displayCtx.getDisplay(overlay) saveOverlay(overlay, display)
[docs]def saveOverlay(overlay, display=None): """Saves the currently selected overlay (only if it is a :class:`.Image`), by a call to :meth:`.Image.save`. If a ``display`` is provided, the :attr:`.Display.name` may be updated to match the new overlay file name. :arg overlay: The :class:`.Image` overlay to save :arg display: The :class:`.Display` instance associated with the overlay. """ import wx # TODO support for other overlay types if not isinstance(overlay, fslimage.Image): raise RuntimeError('Non-volumetric types not supported yet') # If this image has been loaded from a file, # ask the user whether they want to overwrite # that file, or save the image to a new file. # if overlay.dataSource is not None: # If the data source is not nifti (e.g. # mgz), we are not going to overwrite it, # so we don't ask. if fslimage.looksLikeImage(overlay.dataSource): msg = strings.messages['SaveOverlayAction.overwrite'].format( overlay.dataSource) title = strings.titles[ 'SaveOverlayAction.overwrite'].format( overlay.dataSource) dlg = wx.MessageDialog( wx.GetTopLevelWindows()[0], message=msg, caption=title, style=(wx.ICON_WARNING | wx.YES_NO | wx.CANCEL | wx.NO_DEFAULT)) dlg.SetYesNoCancelLabels( strings.labels['SaveOverlayAction.overwrite'], strings.labels['SaveOverlayAction.saveNew'], strings.labels['SaveOverlayAction.cancel']) response = dlg.ShowModal() # Cancel == cancel the save # Yes == overwrite the existing file # No == save to a new file (prompt the user for the file name) if response == wx.ID_CANCEL: return if response == wx.ID_YES: doSave(overlay) return fromDir = op.dirname(overlay.dataSource) filename = fslimage.removeExt(op.basename(overlay.dataSource)) filename = '{}_copy'.format(filename) else: fromDir = fslsettings.read('loadSaveOverlayDir', os.getcwd()) if display is not None: filename = display.name else: filename = overlay.name filename = filename.replace('/', '_') filename = filename.replace('\\', '_') # Ask the user where they # want to save the image msg = strings.titles['SaveOverlayAction.saveFile'] dlg = wx.FileDialog(wx.GetApp().GetTopWindow(), message=msg, defaultDir=fromDir, defaultFile=filename, style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) if dlg.ShowModal() != wx.ID_OK: return # Make sure that the user chose a supported # extension. If not, use the default extension. savePath = dlg.GetPath() prefix, suffix = fslimage.splitExt(savePath) if suffix == '': savePath = '{}{}'.format(prefix, fslimage.defaultExt()) oldPath = overlay.dataSource saveDir = op.dirname(savePath) if doSave(overlay, savePath): # Cache the save directory for next time. fslsettings.write('loadSaveOverlayDir', saveDir) # If image was in memory, or its old # name equalled the old datasource # base name, update its name. if oldPath is None or \ fslimage.removeExt(op.basename(oldPath)) == overlay.name: overlay.name = fslimage.removeExt(op.basename(savePath)) if display is not None: display.name = overlay.name
[docs]def doSave(overlay, path=None): """Called by :func:`saveOverlay`. Tries to save the given ``overlay`` to the given ``path``, and shows an error message if something goes wrong. Returns ``True`` if the save was successful, ``False`` otherwise. """ emsg = strings.messages['SaveOverlayAction.saveError'].format(path) etitle = strings.titles[ 'SaveOverlayAction.saveError'] with status.reportIfError(msg=emsg, title=etitle, raiseError=False): overlay.save(path) return True return False
[docs]def checkOverlaySaveState(overlayList, displayCtx): """Returns ``True`` if all (compatible) overlays are saved to disk, ``False`` if there are any overlays with unsaved changes. """ unsaved = [] for ovl in overlayList: # Only Image overlays can be edited/saved if not isinstance(ovl, fslimage.Image): continue if ovl.saveState: continue unsaved.append(ovl) return len(unsaved) == 0