1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
|
from __future__ import absolute_import, division, print_function
import logging
from abc import ABCMeta, abstractmethod
from glue.utils import CallbackMixin
from glue.core.data_factories import load_data
MAX_UNDO = 50
"""
The classes in this module allow user actions to be stored as commands,
which can be undone/redone
All UI frontends should map interactions to command objects, instead
of directly performing an action.
Commands have access to two sources of data: the first are the
keyword arguments passed to the constructor. These are stored as
attributes of self. The second is a session object passed to all
Command.do and Command.undo calls.
"""
class Command(object):
"""
A class to encapsulate (and possibly undo) state changes
Subclasses of this abstract base class must implement the
`do` and `undo` methods.
Both `do` and `undo` receive a single input argument named
`session` -- this is whatever object is passed to the constructor
of :class:`glue.core.command.CommandStack`. This object is used
to store and retrieve resources needed by each command. The
Glue application itself uses a :class:`~glue.core.session.Session`
instance for this.
Each class should also override the class-level kwargs list,
to list the required keyword arguments that should be passed to the
command constructor. The base class will check that these
keywords are indeed provided. Commands should not take
non-keyword arguments in the constructor method
"""
__metaclass__ = ABCMeta
kwargs = []
def __init__(self, **kwargs):
kwargs = kwargs.copy()
for k in self.kwargs:
if k not in kwargs:
raise RuntimeError("Required keyword %s not passed to %s" %
(k, type(self)))
setattr(self, k, kwargs.pop(k))
self.extra = kwargs
@abstractmethod
def do(self, session):
"""
Execute the command
:param session: An object used to store and fetch resources
needed by a Command.
"""
pass
@abstractmethod
def undo(self, session):
pass
@property
def label(self):
return type(self).__name__
class CommandStack(CallbackMixin):
"""
The command stack collects commands,
and saves them to enable undoing/redoing
After instantiation, something can be assigned to
the session property. This is passed as the sole argument
of all Command (un)do methods.
"""
def __init__(self):
super(CommandStack, self).__init__()
self._session = None
self._command_stack = []
self._undo_stack = []
@property
def session(self):
return self._session
@session.setter
def session(self, value):
self._session = value
@property
def undo_label(self):
""" Brief label for the command reversed by an undo """
if len(self._command_stack) == 0:
return ''
cmd = self._command_stack[-1]
return cmd.label
@property
def redo_label(self):
""" Brief label for the command executed on a redo"""
if len(self._undo_stack) == 0:
return ''
cmd = self._undo_stack[-1]
return cmd.label
def do(self, cmd):
"""
Execute and log a new command
:rtype: The return value of cmd.do()
"""
logging.getLogger(__name__).debug("Do %s", cmd)
self._command_stack.append(cmd)
result = cmd.do(self._session)
self._command_stack = self._command_stack[-MAX_UNDO:]
self._undo_stack = []
self.notify('do')
return result
def undo(self):
"""
Undo the previous command
:raises: IndexError, if there are no objects to undo
"""
try:
c = self._command_stack.pop()
logging.getLogger(__name__).debug("Undo %s", c)
except IndexError:
raise IndexError("No commands to undo")
self._undo_stack.append(c)
c.undo(self._session)
self.notify('undo')
def redo(self):
"""
Redo the previously-undone command
:raises: IndexError, if there are no undone actions
"""
try:
c = self._undo_stack.pop()
logging.getLogger(__name__).debug("Undo %s", c)
except IndexError:
raise IndexError("No commands to redo")
result = c.do(self._session)
self._command_stack.append(c)
self.notify('redo')
return result
def can_undo_redo(self):
"""
Return whether undo and redo options are possible
:rtype: (bool, bool) - Whether undo and redo are possible, respectively
"""
return len(self._command_stack) > 0, len(self._undo_stack) > 0
class LoadData(Command):
kwargs = ['path', 'factory']
label = 'load data'
def do(self, session):
return load_data(self.path, self.factory)
def undo(self, session):
pass
class AddData(Command):
kwargs = ['data']
label = 'add data'
def do(self, session):
session.data_collection.append(self.data)
def undo(self, session):
session.data_collection.remove(self.data)
class RemoveData(Command):
kwargs = ['data']
label = 'remove data'
def do(self, session):
session.data_collection.remove(self.data)
def undo(self, session):
session.data_collection.append(self.data)
class NewDataViewer(Command):
"""Add a new data viewer to the application
:param viewer: The class of viewer to create
:param data: The data object to initialize the viewer with, or None
:type date: :class:`~glue.core.data.Data` or None
"""
kwargs = ['viewer', 'data']
label = 'new data viewer'
def do(self, session):
v = session.application.new_data_viewer(self.viewer, self.data)
self.created = v
return v
def undo(self, session):
self.created.close(warn=False)
class AddLayer(Command):
"""Add a new layer to a viewer
:param layer: The layer to add
:type layer: :class:`~glue.core.data.Data` or :class:`~glue.core.subset.Subset`
:param viewer: The viewer to add the layer to
"""
kwargs = ['layer', 'viewer']
label = 'add layer'
def do(self, session):
self.viewer.add_layer(self.layer)
def undo(self, session):
self.viewer.remove_layer(self.layer)
class ApplyROI(Command):
"""
Apply an ROI to a client, updating subset states
:param client: Client to work on
:type client: :class:`~glue.core.client.Client`
:param roi: Roi to apply
:type roi: :class:`~glue.core.roi.Roi`
"""
kwargs = ['client', 'roi']
label = 'apply ROI'
def do(self, session):
self.old_states = {}
for data in self.client.data:
for subset in data.subsets:
self.old_states[subset] = subset.subset_state
self.client.apply_roi(self.roi)
def undo(self, session):
for data in self.client.data:
for subset in data.subsets:
if subset not in self.old_states:
subset.delete()
for k, v in self.old_states.items():
k.subset_state = v
class LinkData(Command):
pass
class SetViewState(Command):
pass
class NewTab(Command):
pass
class CloseTab(Command):
pass
class NewSubset(Command):
pass
class CopySubset(Command):
pass
class PasteSubset(Command):
pass
class SpecialPasteSubset(Command):
pass
class DeleteSubset(Command):
pass
class SetStyle(Command):
pass
class SetLabel(Command):
pass
|