Skip to content
This repository has been archived by the owner on Dec 19, 2017. It is now read-only.

History Subsystem

Cory McIlroy edited this page Nov 13, 2015 · 7 revisions

Summary

A description of the history store and how to call actions that update it appropriately.

Overview

For each document, the history store maintains a list of known past and future history states. If the state was created within Design Space, the history store also captures a snapshot of the Document and DocumentExport models at that point in time. When performing an undo/redo to a particular state, if the document is cached it will be loaded into the document store. Otherwise, in the case of history states created in standard photoshop or after some failure which caused the history store to be flushed, and therefore not containing a cached document snapshot, updateDocument is called to obtain a fresh model.

History states can be created by one of three basic patterns:

1: PS initiated change (Rogue)

Example: An on-canvas layer move. In these cases get two events: the specific change event, and a historyState event. When handling the specific event (e.g. "move" event), unless it is guaranteed that the historyState event will be handled first, then you must use the "amendRogue" flag in the payload or use the helper action newHistoryStateRogueSafe to create the state. This will allow the history state to be amended if it was the result of a rogue event, or created otherwise.

Using "move guide" as an example because it is initiated by PS, and DS finds out about it via an event which it handles:

var payload = {
    documentID: document.id,
    guide: guide,
    index: index,
    history: {
        newState: true,
        amendRogue: true
    }
};

return this.dispatchAsync(events.document.history.GUIDE_SET, payload)
    .bind(this)
    .then(function () {
        return this.transfer(resetGuidePolicies);
    });

2: DS initiated "optimistic" change

Here the action can, independent of calling photoshop, emit a single flux event which is handled by both the document store and the history store.

An example is deleting an export asset:

var payload = {
    documentID: documentID,
    layerIDs: layerIDs,
    assetIndex: assetIndex,
    history: {
        newState: true,
        name: strings.ACTIONS.MODIFY_EXPORT_ASSETS
    }
};

return this.dispatchAsync(events.export.history.DELETE_ASSET, payload);

3: DS initiated "non optimistic" change

The action makes a change in photoshop but then has to parse the play results, or perform some separate "queries" (eg, resetBounds), before updating our model accordingly. A history state should be created as early as possible (such as with the convenience action newHistoryState()), and then the subsequent events should amend the history state. Note that resetBounds, resetLayers and resetIndex all amend history in this way.

Rotating a set of layers is an example because this will cause the bounds to change:

var documentRef = documentLib.referenceBy.id(document.id),
    layerRef = layers.map(function (layer) {
            return layerLib.referenceBy.id(layer.id);
        })
        .unshift(documentRef)
        .toArray(),
    rotateObj = layerLib.rotate(layerRef, angle),
    options = {
        historyStateInfo: {
            name: strings.ACTIONS.ROTATE_LAYERS,
            target: documentLib.referenceBy.id(document.id)
        }
    },
    historyPromise = this.transfer(historyActions.newHistoryState, document.id,
        strings.ACTIONS.ROTATE_LAYERS),
    playPromise = locking.playWithLockOverride(document, layers, rotateObj, options);

return Promise.join(historyPromise, playPromise)
    .bind(this)
    .then(function () {
        var descendants = layers.flatMap(document.layers.descendants, document.layers);

        return this.transfer(layerActions.resetBounds, document, descendants);
    });

History store events

The history store listens to ALL events with events.document.history and events.export.history. By default, it will amend the current history state with the current Document. By supplying a history property on the payload, you can cause the history store to create a new history state while handling the event (by supplying a subprop history.newState with truthy value). Additionally, the property history.amendRogue will cause the history store to inspect the current history state, and amend it IFF it was created roguely.

Subtleties

A note about historyState events: We currently receive historyState events in most cases. In some cases for scenario 2 and 3, we have lazily relied on the historyState event to increment the history store. For PS reasons, we may soon move away from getting these events if they are a result of an explicit descriptor played through DS. So, we are trying to phase out these code paths in DS. This has the added benefit of providing a more positive/declarative workflow; easier to reason what's going on.

A note about historyStateInfo play option: See SpacesExtension.js for more details. This play option is used to explicitly combine several play commands into one single history state. Example:

playOptions = {
    historyStateInfo: {
        name: strings.ACTIONS.APPLY_TEXT_STYLE,
        target: documentLib.referenceBy.id(currentDocument.id)
    }
};

Visibility Overlay

Not all properties of the DS document model should be loaded during history navigation (undo/redo). Layer selection and visibility are both fetched from Photoshop during history navigation, because it is non-trivial to know how Photoshop will apply them. Guide and Smart Guide visibility are propagated from the current state and clobber the cached state values.

Clone this wiki locally