changeset 858:e3c56d4f863f

GuiAdapter : mouse event routing in SDL + split the undo stack from the ViewportController for multi-canvas apps + adapted the samples to this change
author Benjamin Golinvaux <bgo@osimis.io>
date Mon, 24 Jun 2019 10:31:04 +0200
parents 41d22389a7d2
children 6845a05f9526
files Applications/Generic/GuiAdapter.cpp Applications/Generic/GuiAdapter.h Framework/Scene2DViewport/UndoStack.cpp Framework/Scene2DViewport/UndoStack.h Framework/Scene2DViewport/ViewportController.cpp Framework/Scene2DViewport/ViewportController.h Resources/CMake/OrthancStoneConfiguration.cmake Samples/Sdl/BasicScene.cpp Samples/Sdl/FusionMprSdl.cpp Samples/Sdl/FusionMprSdl.h Samples/Sdl/TrackerSampleApp.cpp Samples/Sdl/TrackerSampleApp.h
diffstat 12 files changed, 455 insertions(+), 117 deletions(-) [+]
line wrap: on
line diff
--- a/Applications/Generic/GuiAdapter.cpp	Wed Jun 19 14:12:28 2019 +0200
+++ b/Applications/Generic/GuiAdapter.cpp	Mon Jun 24 10:31:04 2019 +0200
@@ -41,6 +41,15 @@
     widgets_.push_back(widget);
   }
 
+  std::ostream& operator<<(
+    std::ostream& os, const GuiAdapterKeyboardEvent& event)
+  {
+    os << "ctrl: " << event.ctrlKey << ", " <<
+      "shift: " << event.shiftKey << ", " <<
+      "alt: " << event.altKey;
+    return os;
+  }
+
 #if ORTHANC_ENABLE_WASM == 1
   void GuiAdapter::Run()
   {
@@ -141,7 +150,8 @@
   template<typename GenericFunc>
   struct FuncAdapterPayload
   {
-    void*       userData;
+    std::string canvasId;
+    void* userData;
     GenericFunc callback;
   };
 
@@ -149,7 +159,7 @@
            typename GuiAdapterEvent,
            typename EmscriptenEvent>
   EM_BOOL OnEventAdapterFunc(
-    int eventType, const EmscriptenEvent* wheelEvent, void* userData)
+    int eventType, const EmscriptenEvent* emEvent, void* userData)
   {
 
     // userData is OnMouseWheelFuncAdapterPayload
@@ -162,8 +172,8 @@
     //   " payload->userData: " << payload->userData;
     
     GuiAdapterEvent guiEvent;
-    ConvertFromPlatform(guiEvent, eventType, *wheelEvent);
-    bool ret = (*(payload->callback))(&guiEvent, payload->userData);
+    ConvertFromPlatform(guiEvent, eventType, *emEvent);
+    bool ret = (*(payload->callback))(payload->canvasId, &guiEvent, payload->userData);
     return static_cast<EM_BOOL>(ret);
   }
 
@@ -179,7 +189,7 @@
     
     GuiAdapterEvent guiEvent;
     ConvertFromPlatform(guiEvent, *wheelEvent);
-    bool ret = (*(payload->callback))(&guiEvent, payload->userData);
+    bool ret = (*(payload->callback))(payload->canvasId, &guiEvent, payload->userData);
     return static_cast<EM_BOOL>(ret);
   }
 
@@ -210,6 +220,7 @@
     FuncAdapterPayload<GenericFunc>* payload = 
       new FuncAdapterPayload<GenericFunc>();
     std::auto_ptr<FuncAdapterPayload<GenericFunc> > payloadP(payload);
+    payload->canvasId = canvasId;
     payload->callback = func;
     payload->userData = userData;
     void* userDataRaw = reinterpret_cast<void*>(payload);
@@ -236,6 +247,7 @@
     std::auto_ptr<FuncAdapterPayload<GenericFunc> > payload(
       new FuncAdapterPayload<GenericFunc>()
     );
+    payload->canvasId = canvasId;
     payload->callback = func;
     payload->userData = userData;
     void* userDataRaw = reinterpret_cast<void*>(payload.release());
@@ -250,14 +262,15 @@
   template<
     typename GenericFunc,
     typename EmscriptenSetCallbackFunc>
-    static void SetCallback3(
+    static void SetAnimationFrameCallback(
       EmscriptenSetCallbackFunc emFunc,
       void* userData, GenericFunc func)
   {
-    // LOG(ERROR) << "SetCallback3 !!!!!! (RequestAnimationFrame)";
+    // LOG(ERROR) << "SetAnimationFrameCallback !!!!!! (RequestAnimationFrame)";
     std::auto_ptr<FuncAdapterPayload<GenericFunc> > payload(
       new FuncAdapterPayload<GenericFunc>()
     );
+    payload->canvasId = "UNDEFINED";
     payload->callback = func;
     payload->userData = userData;
     void* userDataRaw = reinterpret_cast<void*>(payload.release());
@@ -352,7 +365,7 @@
     // LOG(ERROR) << "-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+";
     // LOG(ERROR) << "RequestAnimationFrame";
     // LOG(ERROR) << "-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+";
-    SetCallback3<OnAnimationFrameFunc>(
+    SetAnimationFrameCallback<OnAnimationFrameFunc>(
       &emscripten_request_animation_frame_loop,
       userData,
       func);
@@ -384,6 +397,7 @@
 
 #else
 
+// SDL ONLY
 void ConvertFromPlatform(
   GuiAdapterMouseEvent& dest,
   bool ctrlPressed, bool shiftPressed, bool altPressed,
@@ -401,6 +415,9 @@
   case SDL_MOUSEBUTTONUP:
     dest.type = GUIADAPTER_EVENT_MOUSEUP;
     break;
+  case SDL_MOUSEWHEEL:
+    dest.type = GUIADAPTER_EVENT_WHEEL;
+    break;
   default:
     LOG(ERROR) << "SDL event: " << source.type << " is not supported";
     ORTHANC_ASSERT(false, "Not supported");
@@ -441,43 +458,67 @@
   //dest.padding = src.padding;
   }
 
+void ConvertFromPlatform(
+  GuiAdapterWheelEvent& dest,
+  bool ctrlPressed, bool shiftPressed, bool altPressed,
+  const SDL_Event& source)
+{
+  ConvertFromPlatform(dest.mouse, ctrlPressed, shiftPressed, altPressed, source);
+  dest.deltaX = source.wheel.x;
+  dest.deltaY = source.wheel.y;
+}
+
+
+
+  // SDL ONLY
   void GuiAdapter::SetResizeCallback(
     std::string canvasId, void* userData, bool capture, OnWindowResizeFunc func)
   {
-    resizeHandlers_.push_back(std::make_pair(func, userData));
+    resizeHandlers_.push_back(EventHandlerData<OnWindowResizeFunc>(canvasId, func, userData));
   }
 
+  // SDL ONLY
   void GuiAdapter::SetMouseDownCallback(
     std::string canvasId, void* userData, bool capture, OnMouseEventFunc func)
  {
+    mouseDownHandlers_.push_back(EventHandlerData<OnMouseEventFunc>(canvasId, func, userData));
  }
 
+  // SDL ONLY
   void GuiAdapter::SetMouseMoveCallback(
     std::string canvasId, void* userData, bool capture, OnMouseEventFunc  func)
  {
- }
+    mouseMoveHandlers_.push_back(EventHandlerData<OnMouseEventFunc>(canvasId, func, userData));
+  }
 
+  // SDL ONLY
   void GuiAdapter::SetMouseUpCallback(
     std::string canvasId, void* userData, bool capture, OnMouseEventFunc  func)
  {
- }
+    mouseUpHandlers_.push_back(EventHandlerData<OnMouseEventFunc>(canvasId, func, userData));
+  }
 
- void GuiAdapter::SetWheelCallback(
+  // SDL ONLY
+  void GuiAdapter::SetWheelCallback(
    std::string canvasId, void* userData, bool capture, OnMouseWheelFunc  func)
- {
- }
+  {
+    mouseWheelHandlers_.push_back(EventHandlerData<OnMouseWheelFunc>(canvasId, func, userData));
+  }
 
- void GuiAdapter::SetKeyDownCallback(
+  // SDL ONLY
+  void GuiAdapter::SetKeyDownCallback(
    std::string canvasId, void* userData, bool capture, OnKeyDownFunc   func)
  {
  }
 
- void GuiAdapter::SetKeyUpCallback(
+  // SDL ONLY
+  void GuiAdapter::SetKeyUpCallback(
    std::string canvasId, void* userData, bool capture, OnKeyUpFunc    func)
  {
  }
 
 
+  // SDL ONLY
   void GuiAdapter::OnAnimationFrame()
   {
     for (size_t i = 0; i < animationFrameHandlers_.size(); i++)
@@ -487,20 +528,101 @@
     }
   }
 
+  // SDL ONLY
   void GuiAdapter::OnResize()
   {
     for (size_t i = 0; i < resizeHandlers_.size(); i++)
     {
-      // TODO: fix time 
-      (*(resizeHandlers_[i].first))(0, resizeHandlers_[i].second);
+      (*(resizeHandlers_[i].func))(
+        resizeHandlers_[i].canvasName, 0, resizeHandlers_[i].userData);
+    }
+  }
+
+  // SDL ONLY
+  void GuiAdapter::OnMouseWheelEvent(uint32_t windowID, const GuiAdapterWheelEvent& event)
+  {
+
+    // the SDL window name IS the canvas name ("canvas" is used because this lib
+    // is designed for Wasm
+    SDL_Window* sdlWindow = SDL_GetWindowFromID(windowID);
+    ORTHANC_ASSERT(sdlWindow != NULL, "Window ID \"" << windowID << "\" is not a valid SDL window ID!");
+
+    const char* windowTitleSz = SDL_GetWindowTitle(sdlWindow);
+    ORTHANC_ASSERT(windowTitleSz != NULL, "Window ID \"" << windowID << "\" has a NULL window title!");
+
+    std::string windowTitle(windowTitleSz);
+    ORTHANC_ASSERT(windowTitle != "", "Window ID \"" << windowID << "\" has an empty window title!");
+
+    switch (event.mouse.type)
+    {
+    case GUIADAPTER_EVENT_WHEEL:
+      for (size_t i = 0; i < mouseWheelHandlers_.size(); i++)
+      {
+        if(mouseWheelHandlers_[i].canvasName == windowTitle)
+          (*(mouseWheelHandlers_[i].func))(windowTitle, &event, mouseWheelHandlers_[i].userData);
+      }
+      break;
+    default:
+      ORTHANC_ASSERT(false, "Wrong event.type: " << event.mouse.type << " in GuiAdapter::OnMouseWheelEvent(...)");
+      break;
     }
   }
-   
+
+  // SDL ONLY
   void GuiAdapter::OnMouseEvent(uint32_t windowID, const GuiAdapterMouseEvent& event)
   {
+    // the SDL window name IS the canvas name ("canvas" is used because this lib
+    // is designed for Wasm
+    SDL_Window* sdlWindow = SDL_GetWindowFromID(windowID);
+    ORTHANC_ASSERT(sdlWindow != NULL, "Window ID \"" << windowID << "\" is not a valid SDL window ID!");
+     
+    const char* windowTitleSz = SDL_GetWindowTitle(sdlWindow);
+    ORTHANC_ASSERT(windowTitleSz != NULL, "Window ID \"" << windowID << "\" has a NULL window title!");
+
+    std::string windowTitle(windowTitleSz);
+    ORTHANC_ASSERT(windowTitle != "", "Window ID \"" << windowID << "\" has an empty window title!");
+
+    switch (event.type)
+    {
+    case GUIADAPTER_EVENT_MOUSEDOWN:
+      for (size_t i = 0; i < mouseDownHandlers_.size(); i++)
+      {
+        if (mouseDownHandlers_[i].canvasName == windowTitle)
+          (*(mouseDownHandlers_[i].func))(windowTitle, &event, mouseDownHandlers_[i].userData);
+      }
+      break;
+    case GUIADAPTER_EVENT_MOUSEMOVE:
+      for (size_t i = 0; i < mouseMoveHandlers_.size(); i++)
+      {
+        if (mouseMoveHandlers_[i].canvasName == windowTitle)
+          (*(mouseMoveHandlers_[i].func))(windowTitle, &event, mouseMoveHandlers_[i].userData);
+      }
+      break;
+    case GUIADAPTER_EVENT_MOUSEUP:
+      for (size_t i = 0; i < mouseUpHandlers_.size(); i++)
+      {
+        if (mouseUpHandlers_[i].canvasName == windowTitle)
+          (*(mouseUpHandlers_[i].func))(windowTitle, &event, mouseUpHandlers_[i].userData);
+      }
+      break;
+    default:
+      ORTHANC_ASSERT(false, "Wrong event.type: " << event.type << " in GuiAdapter::OnMouseEvent(...)");
+      break;
+    }
+
+    ////boost::shared_ptr<IGuiAdapterWidget> GetWidgetFromWindowId();
+    //boost::shared_ptr<IGuiAdapterWidget> foundWidget;
+    //VisitWidgets([foundWidget, windowID](auto widget)
+    //  {
+    //    if (widget->GetSdlWindowID() == windowID)
+    //      foundWidget = widget;
+    //  });
+    //ORTHANC_ASSERT(foundWidget, "WindowID " << windowID << " was not found in the registered widgets!");
+    //if(foundWidget)
+    //  foundWidget->
   }
 
-
+  // SDL ONLY
   void GuiAdapter::RequestAnimationFrame(OnAnimationFrameFunc func, void* userData)
   {
     animationFrameHandlers_.push_back(std::make_pair(func, userData));
@@ -508,6 +630,7 @@
 
 # if ORTHANC_ENABLE_OPENGL == 1 && !defined(__APPLE__)   /* OpenGL debug is not available on OS X */
 
+  // SDL ONLY
   static void GLAPIENTRY
     OpenGLMessageCallback(GLenum source,
       GLenum type,
@@ -526,6 +649,7 @@
 }
 # endif
 
+  // SDL ONLY
   void GuiAdapter::Run()
   {
 # if ORTHANC_ENABLE_OPENGL == 1 && !defined(__APPLE__)
@@ -591,6 +715,44 @@
           }
 #endif
         }
+        else if (event.type == SDL_MOUSEWHEEL)
+        {
+
+          int scancodeCount = 0;
+          const uint8_t* keyboardState = SDL_GetKeyboardState(&scancodeCount);
+          bool ctrlPressed(false);
+          bool shiftPressed(false);
+          bool altPressed(false);
+
+          if (SDL_SCANCODE_LCTRL < scancodeCount && keyboardState[SDL_SCANCODE_LCTRL])
+            ctrlPressed = true;
+          if (SDL_SCANCODE_RCTRL < scancodeCount && keyboardState[SDL_SCANCODE_RCTRL])
+            ctrlPressed = true;
+          if (SDL_SCANCODE_LSHIFT < scancodeCount && keyboardState[SDL_SCANCODE_LSHIFT])
+            shiftPressed = true;
+          if (SDL_SCANCODE_RSHIFT < scancodeCount && keyboardState[SDL_SCANCODE_RSHIFT])
+            shiftPressed = true;
+          if (SDL_SCANCODE_LALT < scancodeCount && keyboardState[SDL_SCANCODE_LALT])
+            altPressed = true;
+
+          GuiAdapterWheelEvent dest;
+          ConvertFromPlatform(dest, ctrlPressed, shiftPressed, altPressed, event);
+          OnMouseWheelEvent(event.window.windowID, dest);
+
+          //KeyboardModifiers modifiers = GetKeyboardModifiers(keyboardState, scancodeCount);
+
+          //int x, y;
+          //SDL_GetMouseState(&x, &y);
+
+          //if (event.wheel.y > 0)
+          //{
+          //  locker.GetCentralViewport().MouseWheel(MouseWheelDirection_Up, x, y, modifiers);
+          //}
+          //else if (event.wheel.y < 0)
+          //{
+          //  locker.GetCentralViewport().MouseWheel(MouseWheelDirection_Down, x, y, modifiers);
+          //}
+        }
         else if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
         {
 #if 0
--- a/Applications/Generic/GuiAdapter.h	Wed Jun 19 14:12:28 2019 +0200
+++ b/Applications/Generic/GuiAdapter.h	Mon Jun 24 10:31:04 2019 +0200
@@ -17,6 +17,8 @@
  * You should have received a copy of the GNU Affero General Public License
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
+#pragma once
+
 #include <string>
 
 #if ORTHANC_ENABLE_WASM != 1
@@ -62,6 +64,7 @@
   {
   public:
     virtual ~IGuiAdapterWidget() {}
+
   };
 
   enum GuiAdapterMouseEventType
@@ -84,13 +87,13 @@
   class LockingEmitter;
     
 #if 1
-  typedef bool (*OnMouseEventFunc)(const GuiAdapterMouseEvent* mouseEvent, void* userData);
-  typedef bool (*OnMouseWheelFunc)(const GuiAdapterWheelEvent* wheelEvent, void* userData);
-  typedef bool (*OnKeyDownFunc)   (const GuiAdapterKeyboardEvent*   keyEvent,   void* userData);
-  typedef bool (*OnKeyUpFunc)     (const GuiAdapterKeyboardEvent*   keyEvent,   void* userData);
+  typedef bool (*OnMouseEventFunc)(std::string canvasId, const GuiAdapterMouseEvent* mouseEvent, void* userData);
+  typedef bool (*OnMouseWheelFunc)(std::string canvasId, const GuiAdapterWheelEvent* wheelEvent, void* userData);
+  typedef bool (*OnKeyDownFunc)   (std::string canvasId, const GuiAdapterKeyboardEvent*   keyEvent,   void* userData);
+  typedef bool (*OnKeyUpFunc)     (std::string canvasId, const GuiAdapterKeyboardEvent*   keyEvent,   void* userData);
 
   typedef bool (*OnAnimationFrameFunc)(double time, void* userData);
-  typedef bool (*OnWindowResizeFunc)(const GuiAdapterUiEvent* uiEvent, void* userData);
+  typedef bool (*OnWindowResizeFunc)(std::string canvasId, const GuiAdapterUiEvent* uiEvent, void* userData);
 
 #else
 
@@ -154,6 +157,8 @@
     bool altKey;
   };
 
+  std::ostream& operator<<(std::ostream& os, const GuiAdapterKeyboardEvent& event);
+
   /*
     Mousedown event trigger when either the left or right (or middle) mouse is pressed 
     on the object;
@@ -198,6 +203,12 @@
     GuiAdapterMouseEvent& dest,
     bool ctrlPressed, bool shiftPressed, bool altPressed,
     const SDL_Event& source);
+
+  void ConvertFromPlatform(
+    GuiAdapterWheelEvent& dest,
+    bool ctrlPressed, bool shiftPressed, bool altPressed,
+    const SDL_Event& source);
+
 # endif
 
 #endif
@@ -221,9 +232,9 @@
     /**
       emscripten_set_resize_callback("#window", NULL, false, OnWindowResize);
 
-      emscripten_set_wheel_callback("mycanvas1", widget1_.get(), false, OnMouseWheel);
-      emscripten_set_wheel_callback("mycanvas2", widget2_.get(), false, OnMouseWheel);
-      emscripten_set_wheel_callback("mycanvas3", widget3_.get(), false, OnMouseWheel);
+      emscripten_set_wheel_callback("mycanvas1", widget1_.get(), false, OnXXXMouseWheel);
+      emscripten_set_wheel_callback("mycanvas2", widget2_.get(), false, OnXXXMouseWheel);
+      emscripten_set_wheel_callback("mycanvas3", widget3_.get(), false, OnXXXMouseWheel);
 
       emscripten_set_keydown_callback("#window", NULL, false, OnKeyDown);
       emscripten_set_keyup_callback("#window", NULL, false, OnKeyUp);
@@ -295,17 +306,41 @@
     
     void OnResize();
 
-    std::vector<std::pair<OnWindowResizeFunc, void*> >
-      resizeHandlers_;
+#if ORTHANC_ENABLE_SDL == 1
+    template<typename Func>
+    struct EventHandlerData
+    {
+      EventHandlerData(std::string canvasName, Func func, void* userData) 
+        : canvasName(canvasName)
+        , func(func)
+        , userData(userData)
+      {
+      }
+
+      std::string canvasName;
+      Func        func;
+      void*       userData;
+    };
+    std::vector<EventHandlerData<OnWindowResizeFunc> > resizeHandlers_;
+    std::vector<EventHandlerData<OnMouseEventFunc  > > mouseDownHandlers_;
+    std::vector<EventHandlerData<OnMouseEventFunc  > > mouseMoveHandlers_;
+    std::vector<EventHandlerData<OnMouseEventFunc  > > mouseUpHandlers_;
+    std::vector<EventHandlerData<OnMouseWheelFunc  > > mouseWheelHandlers_;
     
 
-#if ORTHANC_ENABLE_SDL == 1
-
     /**
     This executes all the registered headers if needed (in wasm, the browser
     deals with this)
     */
     void OnMouseEvent(uint32_t windowID, const GuiAdapterMouseEvent& event);
+    
+    /**
+    Same remark as OnMouseEvent
+    */
+    void OnMouseWheelEvent(uint32_t windowID, const GuiAdapterWheelEvent& event);
+
+    boost::shared_ptr<IGuiAdapterWidget> GetWidgetFromWindowId();
+
 #endif
 
     /**
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2DViewport/UndoStack.cpp	Mon Jun 24 10:31:04 2019 +0200
@@ -0,0 +1,68 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "UndoStack.h"
+
+#include "MeasureCommands.h"
+
+#include "../StoneException.h"
+
+namespace OrthancStone
+{
+  UndoStack::UndoStack() : numAppliedCommands_(0)
+  {}
+
+  void UndoStack::PushCommand(boost::shared_ptr<TrackerCommand> command)
+  {
+    commandStack_.erase(
+      commandStack_.begin() + numAppliedCommands_,
+      commandStack_.end());
+
+    ORTHANC_ASSERT(std::find(commandStack_.begin(), commandStack_.end(), command)
+      == commandStack_.end(), "Duplicate command");
+    commandStack_.push_back(command);
+    numAppliedCommands_++;
+  }
+
+  void UndoStack::Undo()
+  {
+    ORTHANC_ASSERT(CanUndo(), "");
+    commandStack_[numAppliedCommands_ - 1]->Undo();
+    numAppliedCommands_--;
+  }
+
+  void UndoStack::Redo()
+  {
+    ORTHANC_ASSERT(CanRedo(), "");
+    commandStack_[numAppliedCommands_]->Redo();
+    numAppliedCommands_++;
+  }
+
+  bool UndoStack::CanUndo() const
+  {
+    return numAppliedCommands_ > 0;
+  }
+
+  bool UndoStack::CanRedo() const
+  {
+    return numAppliedCommands_ < commandStack_.size();
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Scene2DViewport/UndoStack.h	Mon Jun 24 10:31:04 2019 +0200
@@ -0,0 +1,77 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include <boost/shared_ptr.hpp>
+
+#include <vector>
+
+namespace OrthancStone
+{
+  class TrackerCommand;
+
+  class UndoStack
+  {
+  public:
+    UndoStack();
+
+    /**
+    Stores a command :
+    - this first trims the undo stack to keep the first numAppliedCommands_
+    - then it adds the supplied command at the top of the undo stack
+
+    In other words, when a new command is pushed, all the undone (and not
+    redone) commands are removed.
+    */
+    void PushCommand(boost::shared_ptr<TrackerCommand> command);
+
+    /**
+    Undoes the command at the top of the undo stack, or throws if there is no
+    command to undo.
+    You can check "CanUndo" first to protect against extraneous redo.
+    */
+    void Undo();
+
+    /**
+    Redoes the command that is just above the last applied command in the undo
+    stack or throws if there is no command to redo.
+    You can check "CanRedo" first to protect against extraneous redo.
+    */
+    void Redo();
+
+    /** selfexpl */
+    bool CanUndo() const;
+
+    /** selfexpl */
+    bool CanRedo() const;
+  
+  private:
+    std::vector<boost::shared_ptr<TrackerCommand> > commandStack_;
+
+    /**
+    This is always between >= 0 and <= undoStack_.size() and gives the
+    position where the controller is in the undo stack.
+    - If numAppliedCommands_ > 0, one can undo
+    - If numAppliedCommands_ < numAppliedCommands_.size(), one can redo
+    */
+    size_t                      numAppliedCommands_;
+  };
+}
--- a/Framework/Scene2DViewport/ViewportController.cpp	Wed Jun 19 14:12:28 2019 +0200
+++ b/Framework/Scene2DViewport/ViewportController.cpp	Mon Jun 24 10:31:04 2019 +0200
@@ -19,6 +19,8 @@
  **/
 
 #include "ViewportController.h"
+
+#include "UndoStack.h"
 #include "MeasureCommands.h"
 
 #include "../StoneException.h"
@@ -27,14 +29,49 @@
 
 namespace OrthancStone
 {
-  ViewportController::ViewportController(MessageBroker& broker)
+  ViewportController::ViewportController(boost::weak_ptr<UndoStack> undoStackW, MessageBroker& broker)
     : IObservable(broker)
-    , numAppliedCommands_(0)
+    , undoStackW_(undoStackW)
     , canvasToSceneFactor_(0.0)
   {
     scene_ = boost::make_shared<Scene2D>();
   }
 
+  boost::shared_ptr<UndoStack> ViewportController::GetUndoStack()
+  {
+    return undoStackW_.lock();
+  }
+
+  boost::shared_ptr<const UndoStack> ViewportController::GetUndoStack() const
+  {
+    return undoStackW_.lock();
+  }
+
+  void ViewportController::PushCommand(boost::shared_ptr<TrackerCommand> command)
+  {
+    GetUndoStack()->PushCommand(command);
+  }
+
+  void ViewportController::Undo()
+  {
+    GetUndoStack()->Undo();
+  }
+
+  void ViewportController::Redo()
+  {
+    GetUndoStack()->Redo();
+  }
+
+  bool ViewportController::CanUndo() const
+  {
+    return GetUndoStack()->CanUndo();
+  }
+
+  bool ViewportController::CanRedo() const
+  {
+    return GetUndoStack()->CanRedo();
+  }
+  
   boost::shared_ptr<const Scene2D> ViewportController::GetScene() const
   {
     return scene_;
@@ -91,42 +128,6 @@
     BroadcastMessage(SceneTransformChanged(*this));
   }
 
-  void ViewportController::PushCommand(boost::shared_ptr<TrackerCommand> command)
-  {
-    commandStack_.erase(
-      commandStack_.begin() + numAppliedCommands_,
-      commandStack_.end());
-    
-    ORTHANC_ASSERT(std::find(commandStack_.begin(), commandStack_.end(), command) 
-      == commandStack_.end(), "Duplicate command");
-    commandStack_.push_back(command);
-    numAppliedCommands_++;
-  }
-
-  void ViewportController::Undo()
-  {
-    ORTHANC_ASSERT(CanUndo(), "");
-    commandStack_[numAppliedCommands_-1]->Undo();
-    numAppliedCommands_--;
-  }
-
-  void ViewportController::Redo()
-  {
-    ORTHANC_ASSERT(CanRedo(), "");
-    commandStack_[numAppliedCommands_]->Redo();
-    numAppliedCommands_++;
-  }
-
-  bool ViewportController::CanUndo() const
-  {
-    return numAppliedCommands_ > 0;
-  }
-
-  bool ViewportController::CanRedo() const
-  {
-    return numAppliedCommands_ < commandStack_.size();
-  }
-
   void ViewportController::AddMeasureTool(boost::shared_ptr<MeasureTool> measureTool)
   {
     ORTHANC_ASSERT(std::find(measureTools_.begin(), measureTools_.end(), measureTool)
--- a/Framework/Scene2DViewport/ViewportController.h	Wed Jun 19 14:12:28 2019 +0200
+++ b/Framework/Scene2DViewport/ViewportController.h	Mon Jun 24 10:31:04 2019 +0200
@@ -30,10 +30,8 @@
 
 namespace OrthancStone
 {
-  /**
-    These constats are used 
-  
-  */
+  class UndoStack;
+
   const double ARC_RADIUS_CANVAS_COORD = 30.0;
   const double TEXT_CENTER_DISTANCE_CANVAS_COORD = 90;
 
@@ -71,7 +69,7 @@
     ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, \
       SceneTransformChanged, ViewportController);
 
-    ViewportController(MessageBroker& broker);
+    ViewportController(boost::weak_ptr<UndoStack> undoStackW, MessageBroker& broker);
 
     boost::shared_ptr<const Scene2D> GetScene() const;
     boost::shared_ptr<Scene2D>      GetScene();
@@ -107,36 +105,6 @@
     /** Forwarded to the underlying scene, and broadcasted to the observers */
     void FitContent(unsigned int canvasWidth, unsigned int canvasHeight);
 
-    /** 
-    Stores a command : 
-    - this first trims the undo stack to keep the first numAppliedCommands_ 
-    - then it adds the supplied command at the top of the undo stack
-
-    In other words, when a new command is pushed, all the undone (and not 
-    redone) commands are removed.
-    */
-    void PushCommand(boost::shared_ptr<TrackerCommand> command);
-
-    /**
-    Undoes the command at the top of the undo stack, or throws if there is no
-    command to undo.
-    You can check "CanUndo" first to protect against extraneous redo.
-    */
-    void Undo();
-
-    /**
-    Redoes the command that is just above the last applied command in the undo
-    stack or throws if there is no command to redo. 
-    You can check "CanRedo" first to protect against extraneous redo.
-    */
-    void Redo();
-
-    /** selfexpl */
-    bool CanUndo() const;
-
-    /** selfexpl */
-    bool CanRedo() const;
-
     /** Adds a new measure tool */
     void AddMeasureTool(boost::shared_ptr<MeasureTool> measureTool);
 
@@ -169,18 +137,31 @@
     */
     double GetAngleTopTextLabelDistanceS() const;
 
+
+    /** forwarded to the UndoStack */
+    void PushCommand(boost::shared_ptr<TrackerCommand> command);
+
+    /** forwarded to the UndoStack */
+    void Undo();
+
+    /** forwarded to the UndoStack */
+    void Redo();
+
+    /** forwarded to the UndoStack */
+    bool CanUndo() const;
+
+    /** forwarded to the UndoStack */
+    bool CanRedo() const;
+
+
   private:
     double GetCanvasToSceneFactor() const;
 
-    std::vector<boost::shared_ptr<TrackerCommand> > commandStack_;
-    
-    /**
-    This is always between >= 0 and <= undoStack_.size() and gives the 
-    position where the controller is in the undo stack. 
-    - If numAppliedCommands_ > 0, one can undo
-    - If numAppliedCommands_ < numAppliedCommands_.size(), one can redo
-    */
-    size_t                      numAppliedCommands_;
+    boost::weak_ptr<UndoStack>                   undoStackW_;
+
+    boost::shared_ptr<UndoStack>                 GetUndoStack();
+    boost::shared_ptr<const UndoStack>           GetUndoStack() const;
+
     std::vector<boost::shared_ptr<MeasureTool> > measureTools_;
     boost::shared_ptr<Scene2D>                   scene_;
     boost::shared_ptr<IFlexiblePointerTracker>   tracker_;
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Wed Jun 19 14:12:28 2019 +0200
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Mon Jun 24 10:31:04 2019 +0200
@@ -491,6 +491,8 @@
   ${ORTHANC_STONE_ROOT}/Framework/Scene2DViewport/OneGesturePointerTracker.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2DViewport/OneGesturePointerTracker.h
   ${ORTHANC_STONE_ROOT}/Framework/Scene2DViewport/PredeclaredTypes.h
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2DViewport/UndoStack.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Scene2DViewport/UndoStack.h
   ${ORTHANC_STONE_ROOT}/Framework/Scene2DViewport/ViewportController.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Scene2DViewport/ViewportController.h
   ${ORTHANC_STONE_ROOT}/Framework/StoneEnumerations.cpp
--- a/Samples/Sdl/BasicScene.cpp	Wed Jun 19 14:12:28 2019 +0200
+++ b/Samples/Sdl/BasicScene.cpp	Mon Jun 24 10:31:04 2019 +0200
@@ -29,6 +29,7 @@
 #include "../../Framework/Scene2D/Scene2D.h"
 #include "../../Framework/Scene2D/ZoomSceneTracker.h"
 #include "../../Framework/Scene2DViewport/ViewportController.h"
+#include "../../Framework/Scene2DViewport/UndoStack.h"
 
 #include "../../Framework/StoneInitialization.h"
 #include "../../Framework/Messages/MessageBroker.h"
@@ -376,8 +377,9 @@
   try
   {
     MessageBroker broker;
+    boost::shared_ptr<UndoStack> undoStack(new UndoStack);
     boost::shared_ptr<ViewportController> controller = boost::make_shared<ViewportController>(
-		boost::ref(broker));
+      undoStack, boost::ref(broker));
     PrepareScene(controller);
     Run(controller);
   }
--- a/Samples/Sdl/FusionMprSdl.cpp	Wed Jun 19 14:12:28 2019 +0200
+++ b/Samples/Sdl/FusionMprSdl.cpp	Mon Jun 24 10:31:04 2019 +0200
@@ -31,6 +31,7 @@
 #include "../../Framework/Scene2D/ZoomSceneTracker.h"
 #include "../../Framework/Scene2D/RotateSceneTracker.h"
 
+#include "../../Framework/Scene2DViewport/UndoStack.h"
 #include "../../Framework/Scene2DViewport/CreateLineMeasureTracker.h"
 #include "../../Framework/Scene2DViewport/CreateAngleMeasureTracker.h"
 #include "../../Framework/Scene2DViewport/IFlexiblePointerTracker.h"
@@ -407,6 +408,7 @@
     , oracleObservable_(broker)
     , oracle_(*this)
     , currentTool_(FusionMprGuiTool_Rotate)
+    , undoStack_(new UndoStack)
   {
     //oracleObservable.RegisterObserverCallback
     //(new Callable
@@ -425,7 +427,7 @@
       <FusionMprSdlApp, OracleCommandExceptionMessage>(*this, &FusionMprSdlApp::Handle));
     
     controller_ = boost::shared_ptr<ViewportController>(
-      new ViewportController(broker_));
+      new ViewportController(undoStack_, broker_));
 
     controller_->RegisterObserverCallback(
       new Callable<FusionMprSdlApp, ViewportController::SceneTransformChanged>
--- a/Samples/Sdl/FusionMprSdl.h	Wed Jun 19 14:12:28 2019 +0200
+++ b/Samples/Sdl/FusionMprSdl.h	Mon Jun 24 10:31:04 2019 +0200
@@ -61,6 +61,7 @@
   static const unsigned int FONT_SIZE_1 = 24;
 
   class Scene2D;
+  class UndoStack;
 
   /**
   This application subclasses IMessageEmitter to use a mutex before forwarding Oracle messages (that
@@ -194,6 +195,8 @@
     int FIXED_INFOTEXT_LAYER_ZINDEX;
 
     FusionMprGuiTool currentTool_;
+    boost::shared_ptr<UndoStack> undoStack_;
+
   };
 
 }
--- a/Samples/Sdl/TrackerSampleApp.cpp	Wed Jun 19 14:12:28 2019 +0200
+++ b/Samples/Sdl/TrackerSampleApp.cpp	Mon Jun 24 10:31:04 2019 +0200
@@ -29,6 +29,7 @@
 #include "../../Framework/Scene2D/RotateSceneTracker.h"
 #include "../../Framework/Scene2D/Scene2D.h"
 #include "../../Framework/Scene2D/ZoomSceneTracker.h"
+#include "../../Framework/Scene2DViewport/UndoStack.h"
 #include "../../Framework/Scene2DViewport/CreateAngleMeasureTracker.h"
 #include "../../Framework/Scene2DViewport/CreateLineMeasureTracker.h"
 #include "../../Framework/StoneInitialization.h"
@@ -458,8 +459,10 @@
 
   TrackerSampleApp::TrackerSampleApp(MessageBroker& broker) : IObserver(broker)
     , currentTool_(GuiTool_Rotate)
+    , undoStack_(new UndoStack)
   {
-    controller_ = boost::shared_ptr<ViewportController>(new ViewportController(broker));
+    controller_ = boost::shared_ptr<ViewportController>(
+      new ViewportController(undoStack_, broker));
 
     controller_->RegisterObserverCallback(
       new Callable<TrackerSampleApp, ViewportController::SceneTransformChanged>
--- a/Samples/Sdl/TrackerSampleApp.h	Wed Jun 19 14:12:28 2019 +0200
+++ b/Samples/Sdl/TrackerSampleApp.h	Mon Jun 24 10:31:04 2019 +0200
@@ -52,6 +52,7 @@
   static const unsigned int FONT_SIZE_1 = 24;
 
   class Scene2D;
+  class UndoStack;
 
   class TrackerSampleApp : public IObserver
     , public boost::enable_shared_from_this<TrackerSampleApp>
@@ -131,6 +132,7 @@
     int FIXED_INFOTEXT_LAYER_ZINDEX;
 
     GuiTool currentTool_;
+    boost::shared_ptr<UndoStack> undoStack_;
   };
 
 }