Skip to content

Commit

Permalink
Refactor undo/redo system to track separate object changes (#2320)
Browse files Browse the repository at this point in the history
`UndoManager`, previously used by the editor, has been removed, and the undo/redo functionality has been migrated to `GameObjectManager`, where the approach taken to save and reproduce object changes is to track each `GameObject`'s data separately.

The new object tracking system can only be enabled inside sectors, which take place in the editor, and can also manually be turned on/off by the user. The maximum amount of saved changes in the undo stack can also be changed by the user. The undo stack is cleaned up when pushing to it and when setting a new maximum limit to undo stack changes.

The virtual `save_state()` and `check_state()` `GameObject` functions can be overriden, meaning that markers or more complex objects can also save other objects' states, so they can be tracked.

Closes #1108.
Fixes #1688.
Closes #1954.
Fixes #2297.
  • Loading branch information
Vankata453 authored Jun 18, 2023
1 parent 2483e27 commit 444d264
Show file tree
Hide file tree
Showing 34 changed files with 701 additions and 404 deletions.
13 changes: 13 additions & 0 deletions src/editor/bezier_marker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "editor/bezier_marker.hpp"

#include "editor/node_marker.hpp"
#include "object/path_gameobject.hpp"
#include "supertux/sector.hpp"

BezierMarker::BezierMarker(Path::Node* node, Vector* bezier_pos) :
Expand Down Expand Up @@ -65,4 +66,16 @@ BezierMarker::get_parent() const
return Sector::current()->get_object_by_uid<NodeMarker>(m_parent);
}

void
BezierMarker::save_state()
{
m_node->get_parent().get_gameobject().save_state();
}

void
BezierMarker::check_state()
{
m_node->get_parent().get_gameobject().check_state();
}

/* EOF */
5 changes: 4 additions & 1 deletion src/editor/bezier_marker.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

class NodeMarker;

class BezierMarker : public MarkerObject
class BezierMarker final : public MarkerObject
{
public:
BezierMarker(Path::Node* node, Vector* bezier_pos);
Expand All @@ -39,6 +39,9 @@ class BezierMarker : public MarkerObject
void set_parent(UID uid) { m_parent = uid; }
NodeMarker* get_parent() const;

void save_state() override;
void check_state() override;

private:
Path::Node* m_node;
Vector* m_pos;
Expand Down
148 changes: 94 additions & 54 deletions src/editor/editor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
#include "editor/tile_selection.hpp"
#include "editor/tip.hpp"
#include "editor/tool_icon.hpp"
#include "editor/undo_manager.hpp"
#include "gui/dialog.hpp"
#include "gui/menu_manager.hpp"
#include "gui/mousecursor.hpp"
Expand Down Expand Up @@ -108,14 +107,13 @@ Editor::Editor() :
m_after_setup(false),
m_tileset(nullptr),
m_widgets(),
m_undo_widget(),
m_redo_widget(),
m_overlay_widget(),
m_toolbox_widget(),
m_layers_widget(),
m_enabled(false),
m_bgr_surface(Surface::from_file("images/engine/menu/bg_editor.png")),
m_undo_manager(new UndoManager),
m_ignore_sector_change(false),
m_level_first_loaded(false),
m_time_since_last_save(0.f),
m_scroll_speed(32.0f)
{
Expand All @@ -127,13 +125,6 @@ Editor::Editor() :
m_layers_widget = layers_widget.get();
m_overlay_widget = overlay_widget.get();

auto undo_button_widget = std::make_unique<ButtonWidget>("images/engine/editor/undo.png",
Vector(10, 10), [this]{ undo(); });
auto redo_button_widget = std::make_unique<ButtonWidget>("images/engine/editor/redo.png",
Vector(60, 10), [this]{ redo(); });

m_widgets.push_back(std::move(undo_button_widget));
m_widgets.push_back(std::move(redo_button_widget));
m_widgets.push_back(std::move(toolbox_widget));
m_widgets.push_back(std::move(layers_widget));
m_widgets.push_back(std::move(overlay_widget));
Expand Down Expand Up @@ -292,7 +283,10 @@ Editor::save_level(const std::string& filename, bool switch_file)
if (switch_file)
m_levelfile = filename;

m_undo_manager->reset_index();
for (const auto& sector : m_level->m_sectors)
{
sector->clear_undo_stack();
}
m_level->save(m_world ? FileSystem::join(m_world->get_basedir(), file) : file);
m_time_since_last_save = 0.f;
remove_autosave_file();
Expand Down Expand Up @@ -443,6 +437,10 @@ Editor::load_sector(const std::string& name)
if (!sector) {
sector = m_level->get_sector(0);
}

sector->set_undo_stack_size(g_config->editor_undo_stack_size);
sector->toggle_undo_tracking(g_config->editor_undo_tracking);

set_sector(sector);
}

Expand Down Expand Up @@ -525,13 +523,6 @@ Editor::set_level(std::unique_ptr<Level> level, bool reset)
m_layers_widget->refresh_sector_text();
m_toolbox_widget->update_mouse_icon();
m_overlay_widget->on_level_change();

if (!m_level_first_loaded)
{
m_undo_manager->try_snapshot(*m_level);
m_undo_manager->reset_index();
m_level_first_loaded = true;
}
}

void
Expand All @@ -544,6 +535,9 @@ Editor::reload_level()
true));
ReaderMapping::s_translations_enabled = true;

retoggle_undo_tracking();
undo_stack_cleanup();

// Autosave files : Once the level is loaded, make sure
// to use the regular file
m_levelfile = get_levelname_from_autosave(m_levelfile);
Expand Down Expand Up @@ -584,11 +578,31 @@ Editor::quit_editor()
void
Editor::check_unsaved_changes(const std::function<void ()>& action)
{
if (m_undo_manager->has_unsaved_changes() && m_levelloaded)
if (!m_levelloaded)
{
action();
return;
}

bool has_unsaved_changes = !g_config->editor_undo_tracking;
if (!has_unsaved_changes)
{
for (const auto& sector : m_level->m_sectors)
{
if (sector->has_object_changes())
{
has_unsaved_changes = true;
break;
}
}
}

if (has_unsaved_changes)
{
m_enabled = false;
auto dialog = std::make_unique<Dialog>();
dialog->set_text(_("This level contains unsaved changes, do you want to save?"));
dialog->set_text(g_config->editor_undo_tracking ? _("This level contains unsaved changes, do you want to save?") :
_("This level may contain unsaved changes, do you want to save?"));
dialog->add_default_button(_("Yes"), [this, action] {
check_save_prerequisites([this, action] {
save_level();
Expand Down Expand Up @@ -735,30 +749,13 @@ Editor::event(const SDL_Event& ev)
return;
}

m_ignore_sector_change = false;

BIND_SECTOR(*m_sector);

for(const auto& widget : m_widgets) {
if (widget->event(ev))
break;
}

// unreliable heuristic to snapshot the current state for future undo
if (((ev.type == SDL_KEYUP && ev.key.repeat == 0 &&
ev.key.keysym.sym != SDLK_LSHIFT &&
ev.key.keysym.sym != SDLK_RSHIFT &&
ev.key.keysym.sym != SDLK_LCTRL &&
ev.key.keysym.sym != SDLK_RCTRL) ||
ev.type == SDL_MOUSEBUTTONUP))
{
if (!m_ignore_sector_change) {
if (m_level) {
m_undo_manager->try_snapshot(*m_level);
}
}
}

// Scroll with mouse wheel, if the mouse is not over the toolbox.
// The toolbox does scrolling independently from the main area.
if (ev.type == SDL_MOUSEWHEEL && !m_toolbox_widget->has_mouse_focus() && !m_layers_widget->has_mouse_focus()) {
Expand Down Expand Up @@ -872,31 +869,74 @@ Editor::check_save_prerequisites(const std::function<void ()>& callback) const
}

void
Editor::undo()
Editor::retoggle_undo_tracking()
{
log_info << "attempting undo" << std::endl;
auto level = m_undo_manager->undo();
if (level) {
set_level(std::move(level), false);
m_ignore_sector_change = true;
} else {
log_info << "undo failed" << std::endl;
if (g_config->editor_undo_tracking && !m_undo_widget)
{
// Add undo/redo button widgets.
auto undo_button_widget = std::make_unique<ButtonWidget>("images/engine/editor/undo.png",
Vector(10, 10), [this]{ undo(); });
auto redo_button_widget = std::make_unique<ButtonWidget>("images/engine/editor/redo.png",
Vector(60, 10), [this]{ redo(); });

m_undo_widget = undo_button_widget.get();
m_redo_widget = redo_button_widget.get();

m_widgets.insert(m_widgets.begin(), std::move(undo_button_widget));
m_widgets.insert(m_widgets.begin() + 1, std::move(redo_button_widget));
}
else if (!g_config->editor_undo_tracking && m_undo_widget)
{
// Remove undo/redo button widgets.
m_widgets.erase(std::remove_if(
m_widgets.begin(), m_widgets.end(),
[this](const std::unique_ptr<Widget>& widget) {
const Widget* ptr = widget.get();
return ptr == m_undo_widget || ptr == m_redo_widget;
}), m_widgets.end());
m_undo_widget = nullptr;
m_redo_widget = nullptr;
}

// Toggle undo tracking for all sectors.
for (const auto& sector : m_level->m_sectors)
sector->toggle_undo_tracking(g_config->editor_undo_tracking);
}

void
Editor::redo()
Editor::undo_stack_cleanup()
{
log_info << "attempting redo" << std::endl;
auto level = m_undo_manager->redo();
if (level) {
set_level(std::move(level), false);
m_ignore_sector_change = true;
} else {
log_info << "redo failed" << std::endl;
// Set the undo stack size and perform undo stack cleanup on all sectors.
for (const auto& sector : m_level->m_sectors)
{
sector->set_undo_stack_size(g_config->editor_undo_stack_size);
sector->undo_stack_cleanup();
}
}

void
Editor::undo()
{
BIND_SECTOR(*m_sector);
m_sector->undo();
post_undo_redo_actions();
}

void
Editor::redo()
{
BIND_SECTOR(*m_sector);
m_sector->redo();
post_undo_redo_actions();
}

void
Editor::post_undo_redo_actions()
{
m_overlay_widget->delete_markers();
m_layers_widget->update_current_tip();
}

IntegrationStatus
Editor::get_status() const
{
Expand Down
14 changes: 8 additions & 6 deletions src/editor/editor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@
#include "util/string_util.hpp"
#include "video/surface_ptr.hpp"

class ButtonWidget;
class GameObject;
class Level;
class ObjectGroup;
class Path;
class Savegame;
class Sector;
class TileSet;
class UndoManager;
class World;

class Editor final : public Screen,
Expand Down Expand Up @@ -152,6 +152,9 @@ class Editor final : public Screen,

Sector* get_sector() { return m_sector; }

void retoggle_undo_tracking();
void undo_stack_cleanup();

void undo();
void redo();

Expand All @@ -172,6 +175,8 @@ class Editor final : public Screen,
void test_level(const std::optional<std::pair<std::string, Vector>>& test_pos);
void update_keyboard(const Controller& controller);

void post_undo_redo_actions();

protected:
std::unique_ptr<Level> m_level;
std::unique_ptr<World> m_world;
Expand Down Expand Up @@ -205,18 +210,15 @@ class Editor final : public Screen,
TileSet* m_tileset;

std::vector<std::unique_ptr<Widget> > m_widgets;
ButtonWidget* m_undo_widget;
ButtonWidget* m_redo_widget;
EditorOverlayWidget* m_overlay_widget;
EditorToolboxWidget* m_toolbox_widget;
EditorLayersWidget* m_layers_widget;

bool m_enabled;
SurfacePtr m_bgr_surface;

std::unique_ptr<UndoManager> m_undo_manager;
bool m_ignore_sector_change;

bool m_level_first_loaded;

float m_time_since_last_save;

float m_scroll_speed;
Expand Down
Loading

0 comments on commit 444d264

Please sign in to comment.