From 0d27b5604fed744e190164a9991e20015384eda1 Mon Sep 17 00:00:00 2001 From: Quentin Quadrat Date: Sun, 17 Dec 2023 17:35:36 +0100 Subject: [PATCH] Add quick and dirty undo/redo actions --- Makefile | 2 +- src/Editor/DearImGui/KeyBindings.hpp | 4 ++ src/Editor/PetriEditor.cpp | 81 +++++++++++++++++++++++++++- src/Editor/PetriEditor.hpp | 37 +++++++++++++ src/Utils/History.cpp | 81 ++++++++++++++++++++++++++++ src/Utils/History.hpp | 63 ++++++++++++++++++++++ 6 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 src/Utils/History.cpp create mode 100644 src/Utils/History.hpp diff --git a/Makefile b/Makefile index 7c92b10..2dec37c 100644 --- a/Makefile +++ b/Makefile @@ -179,7 +179,7 @@ endif # Make the list of compiled files for the application # LIB_OBJS += Path.o Howard.o Utils.o TimedTokens.o Receptivities.o -LIB_OBJS += PetriNet.o Algorithms.o Simulation.o +LIB_OBJS += PetriNet.o Algorithms.o Simulation.o History.o LIB_OBJS += ImportJSON.o ExportJSON.o ExportSymfony.o ExportPnEditor.o LIB_OBJS += ExportPetriLaTeX.o ExportJulia.o ExportGraphviz.o ExportDrawIO.o LIB_OBJS += ExportGrafcetCpp.o diff --git a/src/Editor/DearImGui/KeyBindings.hpp b/src/Editor/DearImGui/KeyBindings.hpp index 488f0d8..8cbc826 100644 --- a/src/Editor/DearImGui/KeyBindings.hpp +++ b/src/Editor/DearImGui/KeyBindings.hpp @@ -22,6 +22,8 @@ # define KEY_BINDINGS_HPP // ----------------------------------------------------------------------------- +// FIXME: The backend raylib does not support other than US keyboard meaning that +// other keyboard mapping are fucked up. # define KEY_QUIT_APPLICATION ImGuiKey_Escape # define KEY_RUN_SIMULATION ImGuiKey_Space # define KEY_RUN_SIMULATION_ALT ImGuiKey_R @@ -30,5 +32,7 @@ # define KEY_MOVE_PETRI_NODE ImGuiKey_Semicolon # define KEY_INCREMENT_TOKENS ImGuiKey_KeypadAdd # define KEY_DECREMENT_TOKENS ImGuiKey_KeypadSubtract +# define KEY_UNDO ImGuiKey_W // remapping for Z +# define KEY_REDO ImGuiKey_Y #endif \ No newline at end of file diff --git a/src/Editor/PetriEditor.cpp b/src/Editor/PetriEditor.cpp index 91a0311..6b3ac16 100644 --- a/src/Editor/PetriEditor.cpp +++ b/src/Editor/PetriEditor.cpp @@ -207,8 +207,19 @@ void Editor::menu() switchOfNet(TypeOfNet(current_type)); ImGui::EndMenu(); } + ImGui::Separator(); + if (ImGui::MenuItem("Undo", "Ctrl+Z", false)) + { + undo(); + } + + if (ImGui::MenuItem("Redo", "Ctrl+Y", false)) + { + redo(); + } + ImGui::Separator(); if (ImGui::MenuItem("Clear net", nullptr, false)) { clearNet(); @@ -960,6 +971,34 @@ void Editor::clearLogs() m_messages.clear(); } +//-------------------------------------------------------------------------- +void Editor::undo() +{ + if (!m_history.undo()) + { + m_messages.setInfo("Cannot do more undos!"); + } + else + { + m_messages.setInfo("Undo!"); + } + m_net.modified = true; +} + +//-------------------------------------------------------------------------- +void Editor::redo() +{ + if (!m_history.redo()) + { + m_messages.setInfo("Cannot do more redos!"); + } + else + { + m_messages.setInfo("Redo!"); + } + m_net.modified = true; +} + //-------------------------------------------------------------------------- Editor::PetriView::PetriView(Editor& editor) : m_editor(editor) @@ -1135,6 +1174,8 @@ void Editor::PetriView::handleAddNode(ImGuiMouseButton button) // present. if (m_editor.getNode(m_mouse.position) == nullptr) { + auto action = std::make_unique(m_editor); + action->before(m_editor.m_net); if (button == ImGuiMouseButton_Left) { m_editor.m_net.addPlace(m_mouse.position.x, @@ -1145,6 +1186,8 @@ void Editor::PetriView::handleAddNode(ImGuiMouseButton button) m_editor.m_net.addTransition(m_mouse.position.x, m_mouse.position.y); } + action->after(m_editor.m_net); + m_editor.m_history.add(std::move(action)); } } else if (m_editor.m_net.type() == TypeOfNet::PetriNet) @@ -1185,6 +1228,9 @@ void Editor::PetriView::handleArcDestination() float x = m_mouse.to->x + (m_mouse.from->x - m_mouse.to->x) / 2.0f; float y = m_mouse.to->y + (m_mouse.from->y - m_mouse.to->y) / 2.0f; float duration = random(1, 5); + + auto action = std::make_unique(m_editor); + action->before(m_editor.m_net); if (m_mouse.to->type == Node::Type::Place) { Transition &n = m_editor.m_net.addTransition(x, y); @@ -1203,6 +1249,8 @@ void Editor::PetriView::handleArcDestination() } m_mouse.from = &n; } + action->after(m_editor.m_net); + m_editor.m_history.add(std::move(action)); } } else @@ -1211,6 +1259,8 @@ void Editor::PetriView::handleArcDestination() // create the origin node before creating the arc. if (m_mouse.arc_from_unknown_node) { + auto action = std::make_unique(m_editor); + action->before(m_editor.m_net); if (m_mouse.to->type == Node::Type::Place) { m_mouse.from = &m_editor.m_net.addTransition( @@ -1221,6 +1271,8 @@ void Editor::PetriView::handleArcDestination() m_mouse.from = &m_editor.m_net.addPlace( m_mouse.click_position.x, m_mouse.click_position.y); } + action->after(m_editor.m_net); + m_editor.m_history.add(std::move(action)); } } } @@ -1236,29 +1288,45 @@ void Editor::PetriView::handleArcDestination() float px = x + (m_mouse.from->x - x) / 2.0f; float py = y + (m_mouse.from->y - y) / 2.0f; float duration = random(1, 5); + auto action = std::make_unique(m_editor); + action->before(m_editor.m_net); Place &n = m_editor.m_net.addPlace(px, py); if (!m_editor.m_net.addArc(*m_mouse.from, n, duration)) { // m_message_bar.setError(m_net.message()); } m_mouse.from = &n; + action->after(m_editor.m_net); + m_editor.m_history.add(std::move(action)); } if (m_mouse.from->type == Node::Type::Place) { + auto action = std::make_unique(m_editor); + action->before(m_editor.m_net); m_mouse.to = &m_editor.m_net.addTransition(x, y); + action->after(m_editor.m_net); + m_editor.m_history.add(std::move(action)); } else { + auto action = std::make_unique(m_editor); + action->before(m_editor.m_net); m_mouse.to = &m_editor.m_net.addPlace(x, y); + action->after(m_editor.m_net); + m_editor.m_history.add(std::move(action)); } } // Create the arc. Note: the duration value is only used // for arc Transition --> Place. + auto action = std::make_unique(m_editor); + action->before(m_editor.m_net); float duration = random(1, 5); if (!m_editor.m_net.addArc(*m_mouse.from, *m_mouse.to, duration)) { // m_message_bar.setError(m_petri_net.message()); } + action->after(m_editor.m_net); + m_editor.m_history.add(std::move(action)); // Reset states m_mouse.from = m_mouse.to = nullptr; @@ -1334,7 +1402,18 @@ void Editor::PetriView::onHandleInput() if (ImGui::IsItemHovered()) { - if (ImGui::IsKeyPressed(KEY_MOVE_PETRI_NODE, false)) + if (ImGui::GetIO().KeyCtrl) + { + if (ImGui::IsKeyPressed(KEY_UNDO, false)) + { + m_editor.undo(); + } + else if (ImGui::IsKeyPressed(KEY_REDO, false)) + { + m_editor.redo(); + } + } + else if (ImGui::IsKeyPressed(KEY_MOVE_PETRI_NODE, false)) { handleMoveNode(); } diff --git a/src/Editor/PetriEditor.hpp b/src/Editor/PetriEditor.hpp index f3371ff..13cf3a9 100644 --- a/src/Editor/PetriEditor.hpp +++ b/src/Editor/PetriEditor.hpp @@ -25,6 +25,7 @@ # include "Net/Simulation.hpp" # include "Net/Exports/Exports.hpp" # include "Net/Imports/Imports.hpp" +# include "Utils/History.hpp" # include "Utils/Path.hpp" namespace tpne { @@ -85,6 +86,8 @@ class Editor: public Application void toogleStartSimulation(); void takeScreenshot(); void clearNet(); + void undo(); + void redo(); private: // Error logs @@ -196,6 +199,38 @@ class Editor: public Application // create. ImVec2 click_position; bool arc_from_unknown_node = false; } m_mouse; + }; // class PetriView + + // ************************************************************************ + //! \brief Quick and dirty net memorization for performing undo/redo. + //! \fixme this is memory usage consuption by saving two nets. It's better + //! to memorize only command. but the remove command make change nodes ID + //! so history will become false. + // ************************************************************************ + class NetModifaction : public History::Action + { + public: + NetModifaction(Editor& editor) : m_editor(editor) {} + void before(Net& net) { m_before = net; } + void after(Net& net) { m_after = net; } + + virtual bool undo() override + { + m_editor.m_net = m_before; + return true; + } + + virtual bool redo() override + { + m_editor.m_net = m_after; + return true; + } + + private: + + Editor& m_editor; + Net m_before; + Net m_after; }; private: @@ -210,6 +245,8 @@ class Editor: public Application //! \brief Single Petri net the editor can edit. //! \fixme Manage several nets (like done with GEMMA). Net m_net; + //! \brief History of modifications of the net. + History m_history; //! \brief Instance allowing to do timed simulation. Simulation m_simulation; //! \brief Visualize the net and do the interaction with the user. diff --git a/src/Utils/History.cpp b/src/Utils/History.cpp new file mode 100644 index 0000000..4a776f0 --- /dev/null +++ b/src/Utils/History.cpp @@ -0,0 +1,81 @@ +#include "Utils/History.hpp" + +namespace tpne { + +bool History::canUndo() const +{ + return m_undoList.size() > 0u; +} + +bool History::canRedo() const +{ + return m_redoList.size() > 0u; +} + +bool History::isDirty() const +{ + return m_nCleanCount != 0u; +} + +bool History::undo() +{ + if (!canUndo()) + return false; + + m_nCleanCount--; + History::Action::Ptr action = std::move(m_undoList.back()); + m_undoList.pop_back(); + if (action->undo()) + { + addRedo(std::move(action)); + return true; + } + return false; +} + +bool History::redo() +{ + if (!canRedo()) + return false; + + m_nCleanCount++; + History::Action::Ptr action = std::move(m_redoList.back()); + m_redoList.pop_back(); + if (action->redo()) + { + addUndo(std::move(action)); + return true; + } + return false; +} + +void History::clear() +{ + m_undoList.clear(); + m_redoList.clear(); + m_nCleanCount = 0u; +} + +void History::addUndo(History::Action::Ptr action) +{ + if (m_undoList.size() >= m_nUndoLevel) + { + m_undoList.pop_front(); + } + m_undoList.push_back(std::move(action)); + if ((m_nCleanCount < 0u) && (m_redoList.size() > 0u)) + { + m_nCleanCount = m_undoList.size() + m_redoList.size() + 1u; + } + else + { + m_nCleanCount++; + } +} + +void History::addRedo(History::Action::Ptr action) +{ + m_redoList.push_back(std::move(action)); +} + +} // namespace tpne \ No newline at end of file diff --git a/src/Utils/History.hpp b/src/Utils/History.hpp new file mode 100644 index 0000000..6d29951 --- /dev/null +++ b/src/Utils/History.hpp @@ -0,0 +1,63 @@ +#ifndef PETRIEDITOR_HISTORY_HPP +# define PETRIEDITOR_HISTORY_HPP + +# include +# include + +namespace tpne { + +// **************************************************************************** +//! \brief Memorize user actions on the Net in the aim to allow undo/redo. +//! Code based on +//! https://www.codeproject.com/Articles/2500/A-Basic-Undo-Redo-Framework-For-C +// **************************************************************************** +class History +{ +public: + + // ************************************************************************ + //! \brief + // ************************************************************************ + class Action + { + public: + using Ptr = std::unique_ptr; + + virtual ~Action() = default; + //! \brief Do the action + virtual bool undo() = 0; + //! \brief Undo the action + virtual bool redo() = 0; + }; + +public: + + History(size_t const nUndoLevel = 10u) + : m_nUndoLevel(nUndoLevel) + {} + + void add(History::Action::Ptr action) { addUndo(std::move(action)); } + bool canUndo() const; + bool undo(); + bool canRedo() const; + bool redo(); + + bool isDirty() const; + void clear(); + +private: + + void addRedo(History::Action::Ptr action); + void addUndo(History::Action::Ptr action); + +private: + + std::list m_undoList; + std::list m_redoList; + size_t m_nUndoLevel; + size_t m_nCleanCount = 0u; +}; + +} // namespace tpne + +#endif \ No newline at end of file