Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

undo/redo extension #311

Closed
wants to merge 5 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions include/clap/ext/draft/undo-redo.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#pragma once

#include "../../plugin.h"
#include "../../string-sizes.h"
#include "../../stream.h"

/// @page Undo/Redo
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few things that are missing

  1. We show the undo/redo stack in our UI. Can we query the host undo/redo stack here?

  2. The host will put other things on the undo redo stack. Do we want to be able to trigger them from the ui and interleave those stacks? I'm thinking especially of things like a setting a parameter from outside the plugin - what does that do as far as undo/redo? We really need to document that case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My approach here would be that the plugin only knows about its internal undo/redo stacks and not the host's stacks. So if the plugin does internal undo/redo, the host might remove those steps from between own undo/redo steps. I think this would simplify things for the plugin devs and the host would be responsible for placing the plugin undo/redo objects in the appropriate place. You think that's OK?

Concerning parameter changes from outside, I would add a note to the documentation that the plugin is always responsible for creating undo steps for parameter changes, even if they come from the host and the host must not create undo steps for those parameter changes (?)

///
/// For each action that causes an incremental change of the plugin state
/// (which includes parameter values and non-parameter state), the plugin provides
/// an undo object to the host.
/// The host then can apply those objects in reverse order in order to
/// undo those actions. Whenever a plugin action is undone (by the host or the plugin),
/// the host receives a redo object which it then can apply in order to redo the action again.
///
/// As opposed to the state extension, undo/redo objects may not contain the
/// whole plugin state but rather only the necessary data for undoing/redoing a
/// single action on top of the current plugin state. Still sometimes it might be the most
/// efficient if an undo/redo object contains the whole plugin state.
///
/// For each plugin-internal state change, undo or redo action, the plugin needs to buffer
/// a pending event object for the host to pull.
/// The host must pull all pending event objects in order to synchronize its undo/redo stack
/// like this:
/// 1. The plugin notifies the host via mark_event_objects_pending.
/// 2. The host calls pull_next_pending_event_object until
/// has_pending_event_object returns false.
///
/// Only if the host undo/redo stacks are in sync, the host can perform an undo/redo on the plugin
/// like this:
/// 1. The host calls begin_apply_event_objects.
/// 2. The host calls apply_event_object for each event it wants to apply.
/// 3. The host calls end_apply_event_objects.

static const char CLAP_EXT_UNDO_REDO[] = "clap.undo-redo.draft/0";

#ifdef __cplusplus
extern "C" {
#endif

enum
{
// The pulled event object represents an internal state change of the plugin.
// The host must put the event object onto its undo stack.
CLAP_UNDO_REDO_OBJECT_CHANGE = 0,
// The pulled event object represents an undo action.
// The host must remove the topmost object from its undo stack and put the pulled
// event object onto its redo stack.
// The host should reassign the clap_change_event_description from the removed object
// to the pulled object as it is complementary to it.
CLAP_UNDO_REDO_OBJECT_UNDO = 1,
// The pulled event object represents a redo action.
// The host must remove the topmost object from its redo stack and put the pulled
// event object onto its undo stack.
// The host should reassign the clap_change_event_description from the removed object
// to the pulled object as it is complementary to it.
CLAP_UNDO_REDO_OBJECT_REDO = 2,
// The pulled event object represents an internal state change of the plugin
// which is already undone again by the time the host pulls it.
// The host must put the event object onto its redo stack.
// This is for optimization purpose as the host otherwise would have to
// pull both a change and a complementary undo object simulate an undo with them.
// In order to minimize the calls of pull_next_pending_event_object, the plugin should
// compress its pending event object buffer like this:
// - Buffer before compression: [Change A, Change B, Change C, Undo C, Undo B]
// - Buffer after compression: [Change A, Undone change C, Undone change B]
// The order of undone changes B and C is important as the host will first put
// undone change C on top of its redo stack and then B on top of that.
CLAP_UNDO_REDO_OBJECT_UNDONE_CHANGE = 3,
};
typedef int32_t clap_undo_redo_object_type;

// Information about a single incremental change event.
// The host can use context and action to embed a message into its own undo/redo
// lists.
typedef struct clap_change_event_description {
// A brief, human-readable description of the event context, for example:
// - "Filter 1 > Cutoff"
// - "Param Seq A"
// - "Load Preset"
// It should not contain the name of the plugin.
char context[CLAP_NAME_SIZE];
// A brief, human-readable description of what happened, for example:
// - "Set to 100%"
// - "Randomized step values"
// - "'<preset_name>'"
char action[CLAP_NAME_SIZE];
} clap_change_event_description_t;

typedef struct clap_plugin_undo_redo{
// Returns true if the plugin has at least one pending undo/redo object not pulled by the
// host via pull_next_pending_event_object yet.
// [main-thread]
bool(CLAP_ABI *has_pending_event_object)(const clap_plugin_t *plugin);

// Pulls an event object into stream in order to sync the host undo/redo stacks
// to the plugin-internal undo/redo stacks.
// For type CLAP_UNDO_REDO_OBJECT_CHANGE and CLAP_UNDO_REDO_OBJECT_UNDONE_CHANGE,
// - the plugin must provide an event description together with the object.
// - the host might omit its redo stack
// For type CLAP_UNDO_REDO_OBJECT_UNDO and CLAP_UNDO_REDO_OBJECT_REDO
// - description is ignored as it can be safely assumed that the host already knows it.
// Returns true if the event object was correctly saved.
// [main-thread]
bool(CLAP_ABI *pull_next_pending_event_object)(const clap_plugin_t *plugin,
clap_undo_redo_object_type* type,
const clap_ostream_t *stream,
clap_change_event_description_t* description);

// Begin applying undo/redo event objects.
// If for_redo is set to false, the plugin will consider the applied events as undo objects,
// otherwise as redo objects.
// The plugin must block internal plugin state changes until the host calls
// end_apply_event_objects.
// The host must not call this function if there are still event objects pending.
// [main-thread && !event_objects_pending]
void(CLAP_ABI *begin_apply_event_objects)(const clap_plugin_t *plugin,
bool for_redo);

// Performs an undo/redo action by applying an event object from apply_object_stream.
// In exchange, the plugin returns the complementary redo/undo event object via
// exchange_object_stream (the 'complementary object').
// The complementary object must satisfy the following condition:
// - Calling clap_plugin_state::load (see state.h) before apply_event_object must return
// the same result as calling clap_plugin_state::load after applying the complementary
// object via apply_event_object
// If begin_apply_event_objects was called with for_redo set to false,
// - the host must remove the applied object from its undo stack
// - the host must add the received complementary object onto its redo stack.
// otherwise
// - the host must remove the applied object from its redo stack
// - the host must add the received complementary object onto its undo stack.
// The host probably wants to re-associate the stored clap_change_event_description from the
// applied object to the received complementary object.
// The plugin must not call mark_event_objects_pending after applying the event object as the
// host and plugin undo/redo stacks should already be synced via the exchange object.
// Returns true if the event objects were transferred correctly.
// [main-thread]
bool(CLAP_ABI *apply_event_object)(const clap_plugin_t *plugin,
const clap_istream_t *apply_object_stream,
const clap_ostream_t *exchange_object_stream);

// End applying event objects
// This allows the plugin to internally update the state in one go after receiving a sequence
// of event objects via apply_event_object.
// The plugin must not call mark_event_objects_pending after applying the event objects as the
// host and plugin undo/redo stacks should already be in sync via the exchange objects.
// [main-thread]
void(CLAP_ABI *end_apply_event_objects)(const clap_plugin_t *plugin);
} clap_plugin_undo_redo_t;

typedef struct clap_host_undo_redo {
// After the plugin state has changed internally, the plugin must call this function to
// tell the host that an event object is pending to be pulled from the plugin.
// The host then must first check has_pending_event_object and call pull_next_pending_event_object
// until has_pending_event_object returns false.
// The plugin does not need to re-send mark_event_objects_pending until the host has
// pulled all pending event objects from the plugin.
// This function should never be called while the host is still applying event objects as the
// plugin is forbidden to change its internal state until end_apply_event_objects arrives.
// [main-thread]
void(CLAP_ABI *mark_event_objects_pending)(const clap_host_t *host);
} clap_host_undo_redo_t;

#ifdef __cplusplus
}
#endif