From 448aa8ab9d77817afe7afbf806300edde2f60e3b Mon Sep 17 00:00:00 2001 From: trinitou Date: Tue, 6 Dec 2022 07:40:35 +0100 Subject: [PATCH 1/5] first draft for undo/redo extension --- include/clap/ext/draft/state-context.h | 2 + include/clap/ext/draft/undo-redo.h | 89 ++++++++++++++++++++++++++ include/clap/ext/state.h | 10 +-- include/clap/stream.h | 12 ++++ 4 files changed, 104 insertions(+), 9 deletions(-) create mode 100644 include/clap/ext/draft/undo-redo.h diff --git a/include/clap/ext/draft/state-context.h b/include/clap/ext/draft/state-context.h index 4912a6e0..fe6fe872 100644 --- a/include/clap/ext/draft/state-context.h +++ b/include/clap/ext/draft/state-context.h @@ -20,6 +20,8 @@ /// clap_plugin_state_context.save(CLAP_STATE_CONTEXT_FOR_PRESET), /// CLAP_STATE_CONTEXT_FOR_PRESET) /// +/// Important: Pay attention to the 'Notes on using streams' as found in stream.h ! +/// /// If the plugin implements CLAP_EXT_STATE_CONTEXT then it is mandatory to also implement /// CLAP_EXT_STATE. diff --git a/include/clap/ext/draft/undo-redo.h b/include/clap/ext/draft/undo-redo.h new file mode 100644 index 00000000..22328d58 --- /dev/null +++ b/include/clap/ext/draft/undo-redo.h @@ -0,0 +1,89 @@ +#pragma once + +#include "../../plugin.h" +#include "../../string-sizes.h" +#include "../../stream.h" + +/// @page Undo/Redo +/// +/// For each action that causes a change of parameter values and non-parameter state +/// (the 'plugin state'), the plugin provides an incremental change event object to the host. +/// These incremental change event objects then can be applied in reverse order in order to +/// undo those actions. If undone, the same objects can be used to redo the action again. +/// +/// As opposed to the state extension, incremental change event objects must 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. +/// +/// Important: Pay attention to the 'Notes on using streams' as found in stream.h ! + +static const char CLAP_EXT_UNDO_REDO[] = "clap.undo-redo.draft/0"; + +#ifdef __cplusplus +extern "C" { +#endif + +// Information about a single incremental change event. +// The host can use context and action to embed a message into its own undo history. +typedef struct clap_incremental_change_event_description { + // A brief, human-readable description of the event context, for example: + // - "Filter 1 > Cutoff" + // - "Param Seq A" + // 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%" + // - "Steps edited" + char action[CLAP_NAME_SIZE]; +} clap_incremental_change_event_description_t; + +typedef struct clap_plugin_undo_redo{ + // Pulls an incremental change event object into stream. + // The plugin must provide an event description together with the object. + // If the host still holds a redo event object, it should compare the received + // undo event object with the next redo object. If it is not equal, the host may + // omit the next and all following redo event objects for that plugin. + // before (besides it wants to support undo history branching). + // Returns true if the incremental change event object was correctly saved. + // [main-thread] + bool(CLAP_ABI *pull_undo_event_object)(const clap_plugin_t *plugin, + const clap_ostream_t *stream, + clap_incremental_change_event_description_t* description); + + // Applies an incremental change event object from stream in order to undo the corresponding + // plugin action. + // The host should move the incremental change event object onto its redo stack. + // Returns true if the incremental change event object was correctly applied. + // The plugin must not call notify_undo as this is implied by returning true already. + // [main-thread] + bool(CLAP_ABI *perform_undo)(const clap_plugin_t *plugin, + const clap_istream_t *stream); + + // Applies an incremental change event object from stream in order to redo the corresponding + // plugin action. + // The host should move the incremental change event object onto its undo stack. + // Returns true if the incremental change event object was correctly applied. + // The plugin must not call mark_dirty_for_undo as this is implied by returning true already. + // [main-thread] + bool(CLAP_ABI *perform_redo)(const clap_plugin_t *plugin, + const clap_istream_t *stream); +} clap_plugin_undo_redo_t; + +typedef struct clap_host_undo_redo { + // Tell the host that the plugin state has changed internally and + // an incremental change event object is ready to be pulled from the plugin. + // The host must then call pull_undo_event_object. + // [main-thread] + void(CLAP_ABI *mark_dirty_for_undo)(const clap_host_t *host); + + // Tell the host that the plugin has performed a single undo step internally. + // The host then must roll back the last undo event for this plugin instance. + // If multiple undo steps have been performed at once inside the plugin, + // it must call this function multiple times as well. + // [main-thread] + void(CLAP_ABI *notify_undo)(const clap_host_t *host); +} clap_host_undo_redo_t; + +#ifdef __cplusplus +} +#endif diff --git a/include/clap/ext/state.h b/include/clap/ext/state.h index 0166fd8d..4e3339af 100644 --- a/include/clap/ext/state.h +++ b/include/clap/ext/state.h @@ -11,15 +11,7 @@ /// between project reloads, when duplicating and copying plugin instances, and /// for host-side preset management. /// -/// ## Notes on using streams -/// -/// When working with `clap_istream` and `clap_ostream` objects to load and save -/// state, it is important to keep in mind that the host may limit the number of -/// bytes that can be read or written at a time. The return values for the -/// stream read and write functions indicate how many bytes were actually read -/// or written. You need to use a loop to ensure that you read or write the -/// entirety of your state. Don't forget to also consider the negative return -/// values for the end of file and IO error codes. +/// Important: Pay attention to the 'Notes on using streams' as found in stream.h ! static CLAP_CONSTEXPR const char CLAP_EXT_STATE[] = "clap.state"; diff --git a/include/clap/stream.h b/include/clap/stream.h index 3e8d9a91..b9b0b6cf 100644 --- a/include/clap/stream.h +++ b/include/clap/stream.h @@ -3,6 +3,18 @@ #include "private/std.h" #include "private/macros.h" +/// @page Streams +/// +/// ## Notes on using streams +/// +/// When working with `clap_istream` and `clap_ostream` objects to load and save +/// state, it is important to keep in mind that the host may limit the number of +/// bytes that can be read or written at a time. The return values for the +/// stream read and write functions indicate how many bytes were actually read +/// or written. You need to use a loop to ensure that you read or write the +/// entirety of your state. Don't forget to also consider the negative return +/// values for the end of file and IO error codes. + #ifdef __cplusplus extern "C" { #endif From b6878ad0ff82ec326d80518ea7fc644c99ca3073 Mon Sep 17 00:00:00 2001 From: trinitou Date: Sat, 11 Mar 2023 12:44:29 +0100 Subject: [PATCH 2/5] remove stream documentation changes in favor of a separate pull request --- include/clap/ext/draft/state-context.h | 2 -- include/clap/ext/draft/undo-redo.h | 2 -- include/clap/ext/state.h | 10 +++++++++- include/clap/stream.h | 12 ------------ 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/include/clap/ext/draft/state-context.h b/include/clap/ext/draft/state-context.h index fe6fe872..4912a6e0 100644 --- a/include/clap/ext/draft/state-context.h +++ b/include/clap/ext/draft/state-context.h @@ -20,8 +20,6 @@ /// clap_plugin_state_context.save(CLAP_STATE_CONTEXT_FOR_PRESET), /// CLAP_STATE_CONTEXT_FOR_PRESET) /// -/// Important: Pay attention to the 'Notes on using streams' as found in stream.h ! -/// /// If the plugin implements CLAP_EXT_STATE_CONTEXT then it is mandatory to also implement /// CLAP_EXT_STATE. diff --git a/include/clap/ext/draft/undo-redo.h b/include/clap/ext/draft/undo-redo.h index 22328d58..5bf73991 100644 --- a/include/clap/ext/draft/undo-redo.h +++ b/include/clap/ext/draft/undo-redo.h @@ -14,8 +14,6 @@ /// As opposed to the state extension, incremental change event objects must 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. -/// -/// Important: Pay attention to the 'Notes on using streams' as found in stream.h ! static const char CLAP_EXT_UNDO_REDO[] = "clap.undo-redo.draft/0"; diff --git a/include/clap/ext/state.h b/include/clap/ext/state.h index 4e3339af..0166fd8d 100644 --- a/include/clap/ext/state.h +++ b/include/clap/ext/state.h @@ -11,7 +11,15 @@ /// between project reloads, when duplicating and copying plugin instances, and /// for host-side preset management. /// -/// Important: Pay attention to the 'Notes on using streams' as found in stream.h ! +/// ## Notes on using streams +/// +/// When working with `clap_istream` and `clap_ostream` objects to load and save +/// state, it is important to keep in mind that the host may limit the number of +/// bytes that can be read or written at a time. The return values for the +/// stream read and write functions indicate how many bytes were actually read +/// or written. You need to use a loop to ensure that you read or write the +/// entirety of your state. Don't forget to also consider the negative return +/// values for the end of file and IO error codes. static CLAP_CONSTEXPR const char CLAP_EXT_STATE[] = "clap.state"; diff --git a/include/clap/stream.h b/include/clap/stream.h index b9b0b6cf..3e8d9a91 100644 --- a/include/clap/stream.h +++ b/include/clap/stream.h @@ -3,18 +3,6 @@ #include "private/std.h" #include "private/macros.h" -/// @page Streams -/// -/// ## Notes on using streams -/// -/// When working with `clap_istream` and `clap_ostream` objects to load and save -/// state, it is important to keep in mind that the host may limit the number of -/// bytes that can be read or written at a time. The return values for the -/// stream read and write functions indicate how many bytes were actually read -/// or written. You need to use a loop to ensure that you read or write the -/// entirety of your state. Don't forget to also consider the negative return -/// values for the end of file and IO error codes. - #ifdef __cplusplus extern "C" { #endif From 044238867f0fe3e8b3d51b140c1a8fc39590fb98 Mon Sep 17 00:00:00 2001 From: trinitou Date: Mon, 13 Mar 2023 09:25:29 +0100 Subject: [PATCH 3/5] more work on undo/redo extension --- include/clap/ext/draft/undo-redo.h | 147 ++++++++++++++++++++--------- 1 file changed, 100 insertions(+), 47 deletions(-) diff --git a/include/clap/ext/draft/undo-redo.h b/include/clap/ext/draft/undo-redo.h index 5bf73991..20fea66d 100644 --- a/include/clap/ext/draft/undo-redo.h +++ b/include/clap/ext/draft/undo-redo.h @@ -6,14 +6,28 @@ /// @page Undo/Redo /// -/// For each action that causes a change of parameter values and non-parameter state -/// (the 'plugin state'), the plugin provides an incremental change event object to the host. -/// These incremental change event objects then can be applied in reverse order in order to -/// undo those actions. If undone, the same objects can be used to redo the action again. +/// 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, incremental change event objects must 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. +/// 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 +/// via apply_event_object. static const char CLAP_EXT_UNDO_REDO[] = "clap.undo-redo.draft/0"; @@ -21,65 +35,104 @@ static const char CLAP_EXT_UNDO_REDO[] = "clap.undo-redo.draft/0"; 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 put the event object onto its redo stack. + CLAP_UNDO_REDO_OBJECT_UNDO = 1, + // The pulled event object represents a redo action. + // The host must put the event object onto its undo stack. + 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 history. -typedef struct clap_incremental_change_event_description { +// 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%" - // - "Steps edited" + // - "Randomized step values" + // - "''" char action[CLAP_NAME_SIZE]; -} clap_incremental_change_event_description_t; +} clap_change_event_description_t; typedef struct clap_plugin_undo_redo{ - // Pulls an incremental change event object into stream. - // The plugin must provide an event description together with the object. - // If the host still holds a redo event object, it should compare the received - // undo event object with the next redo object. If it is not equal, the host may - // omit the next and all following redo event objects for that plugin. - // before (besides it wants to support undo history branching). - // Returns true if the incremental change event object was correctly saved. + // 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 *pull_undo_event_object)(const clap_plugin_t *plugin, - const clap_ostream_t *stream, - clap_incremental_change_event_description_t* description); + bool(CLAP_ABI *has_pending_event_object)(const clap_plugin_t *plugin); - // Applies an incremental change event object from stream in order to undo the corresponding - // plugin action. - // The host should move the incremental change event object onto its redo stack. - // Returns true if the incremental change event object was correctly applied. - // The plugin must not call notify_undo as this is implied by returning true already. + // 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 *perform_undo)(const clap_plugin_t *plugin, - const clap_istream_t *stream); + 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); - // Applies an incremental change event object from stream in order to redo the corresponding - // plugin action. - // The host should move the incremental change event object onto its undo stack. - // Returns true if the incremental change event object was correctly applied. - // The plugin must not call mark_dirty_for_undo as this is implied by returning true already. - // [main-thread] - bool(CLAP_ABI *perform_redo)(const clap_plugin_t *plugin, - const clap_istream_t *stream); + // 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 is_redo is false, + // - the host must remove the applied object from its undo stack + // - the host must add the complementary object onto its redo stack. + // otherwise + // - the host must remove the applied object from its redo stack + // - the host must add the 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 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. + // The host must not call this function if there are still event objects pending. + // Returns true if the change event object was correctly applied. + // [main-thread && !event_objects_pending] + bool(CLAP_ABI *apply_event_object)(const clap_plugin_t *plugin, + bool is_redo, + const clap_istream_t *apply_object_stream, + const clap_ostream_t *exchange_object_stream); } clap_plugin_undo_redo_t; typedef struct clap_host_undo_redo { - // Tell the host that the plugin state has changed internally and - // an incremental change event object is ready to be pulled from the plugin. - // The host must then call pull_undo_event_object. - // [main-thread] - void(CLAP_ABI *mark_dirty_for_undo)(const clap_host_t *host); - - // Tell the host that the plugin has performed a single undo step internally. - // The host then must roll back the last undo event for this plugin instance. - // If multiple undo steps have been performed at once inside the plugin, - // it must call this function multiple times as well. + // 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. // [main-thread] - void(CLAP_ABI *notify_undo)(const clap_host_t *host); + void(CLAP_ABI *mark_event_objects_pending)(const clap_host_t *host); } clap_host_undo_redo_t; #ifdef __cplusplus From 83ada779b032b9935a8de886e265e4c392bf1106 Mon Sep 17 00:00:00 2001 From: trinitou Date: Tue, 14 Mar 2023 08:40:12 +0100 Subject: [PATCH 4/5] some missing notes about pulling undo/redo event objects --- include/clap/ext/draft/undo-redo.h | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/include/clap/ext/draft/undo-redo.h b/include/clap/ext/draft/undo-redo.h index 20fea66d..89052f7d 100644 --- a/include/clap/ext/draft/undo-redo.h +++ b/include/clap/ext/draft/undo-redo.h @@ -41,10 +41,16 @@ enum // 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 put the event object onto its redo stack. + // 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 put the event object onto its undo stack. + // 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. From 658ead13e61fef6100b815e8341d300c9707ae35 Mon Sep 17 00:00:00 2001 From: trinitou Date: Wed, 15 Mar 2023 05:44:48 +0100 Subject: [PATCH 5/5] mechanism for applying multiple undo/redo objects at once --- include/clap/ext/draft/undo-redo.h | 39 +++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/include/clap/ext/draft/undo-redo.h b/include/clap/ext/draft/undo-redo.h index 89052f7d..5b429eae 100644 --- a/include/clap/ext/draft/undo-redo.h +++ b/include/clap/ext/draft/undo-redo.h @@ -27,7 +27,10 @@ /// 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 -/// via apply_event_object. +/// 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"; @@ -104,6 +107,16 @@ typedef struct clap_plugin_undo_redo{ 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'). @@ -111,23 +124,29 @@ typedef struct clap_plugin_undo_redo{ // - 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 is_redo is false, + // 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 complementary object onto its redo 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 complementary object onto its undo 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 complementary object. + // 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. - // The host must not call this function if there are still event objects pending. - // Returns true if the change event object was correctly applied. - // [main-thread && !event_objects_pending] + // Returns true if the event objects were transferred correctly. + // [main-thread] bool(CLAP_ABI *apply_event_object)(const clap_plugin_t *plugin, - bool is_redo, 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 { @@ -137,6 +156,8 @@ typedef struct clap_host_undo_redo { // 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;