diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index cc1f74c539..2cbe996e3c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -976,7 +976,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _window(new MainWindow(desktop())), _sessionRunTimer(startupTimer), _previousSessionCrashed(setupEssentials(argc, argv, runningMarkerExisted)), - _undoStackScriptingInterface(&_undoStack), _entitySimulation(new PhysicalEntitySimulation()), _physicsEngine(new PhysicsEngine(Vectors::ZERO)), _entityClipboard(new EntityTree()), @@ -3095,7 +3094,6 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { surfaceContext->setContextProperty("DialogsManager", _dialogsManagerScriptingInterface); surfaceContext->setContextProperty("FaceTracker", DependencyManager::get().data()); surfaceContext->setContextProperty("AvatarManager", DependencyManager::get().data()); - surfaceContext->setContextProperty("UndoStack", &_undoStackScriptingInterface); surfaceContext->setContextProperty("LODManager", DependencyManager::get().data()); surfaceContext->setContextProperty("HMD", DependencyManager::get().data()); surfaceContext->setContextProperty("Scene", DependencyManager::get().data()); @@ -6767,8 +6765,6 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerGlobalObject("AvatarManager", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("UndoStack", &_undoStackScriptingInterface); - scriptEngine->registerGlobalObject("LODManager", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Paths", DependencyManager::get().data()); diff --git a/interface/src/Application.h b/interface/src/Application.h index f7c9d60429..f2e3b3953f 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -23,7 +23,6 @@ #include #include -#include #include #include @@ -70,7 +69,6 @@ #include "ui/OctreeStatsDialog.h" #include "ui/OverlayConductor.h" #include "ui/overlays/Overlays.h" -#include "UndoStackScriptingInterface.h" #include "workload/GameWorkload.h" @@ -189,7 +187,6 @@ public: const OctreePacketProcessor& getOctreePacketProcessor() const { return _octreeProcessor; } QSharedPointer getEntities() const { return DependencyManager::get(); } - QUndoStack* getUndoStack() { return &_undoStack; } MainWindow* getWindow() const { return _window; } EntityTreePointer getEntityClipboard() const { return _entityClipboard; } EntityEditPacketSender* getEntityEditPacketSender() { return &_entityEditSender; } @@ -571,9 +568,6 @@ private: bool _activatingDisplayPlugin { false }; - QUndoStack _undoStack; - UndoStackScriptingInterface _undoStackScriptingInterface; - uint32_t _renderFrameCount { 0 }; // Frame Rate Measurement diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 7a310e675f..eef14c873e 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -90,19 +90,6 @@ Menu::Menu() { // Edit menu ---------------------------------- MenuWrapper* editMenu = addMenu("Edit"); - // Edit > Undo - QUndoStack* undoStack = qApp->getUndoStack(); - QAction* undoAction = undoStack->createUndoAction(editMenu); - undoAction->setShortcut(Qt::CTRL | Qt::Key_Z); - addActionToQMenuAndActionHash(editMenu, undoAction); - - // Edit > Redo - QAction* redoAction = undoStack->createRedoAction(editMenu); - redoAction->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_Z); - addActionToQMenuAndActionHash(editMenu, redoAction); - - editMenu->addSeparator(); - // Edit > Cut auto cutAction = addActionToQMenuAndActionHash(editMenu, "Cut", QKeySequence::Cut); connect(cutAction, &QAction::triggered, [] { diff --git a/libraries/script-engine/src/UndoStackScriptingInterface.cpp b/libraries/script-engine/src/UndoStackScriptingInterface.cpp deleted file mode 100644 index 1171625c04..0000000000 --- a/libraries/script-engine/src/UndoStackScriptingInterface.cpp +++ /dev/null @@ -1,66 +0,0 @@ -// -// UndoStackScriptingInterface.cpp -// libraries/script-engine/src -// -// Created by Ryan Huffman on 10/22/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "UndoStackScriptingInterface.h" - -#include -#include -#include -#include - -UndoStackScriptingInterface::UndoStackScriptingInterface(QUndoStack* undoStack) : _undoStack(undoStack) { -} - -void UndoStackScriptingInterface::pushCommand(QScriptValue undoFunction, QScriptValue undoData, - QScriptValue redoFunction, QScriptValue redoData) { - if (undoFunction.engine()) { - ScriptUndoCommand* undoCommand = new ScriptUndoCommand(undoFunction, undoData, redoFunction, redoData); - undoCommand->moveToThread(undoFunction.engine()->thread()); - _undoStack->push(undoCommand); - } -} - -ScriptUndoCommand::ScriptUndoCommand(QScriptValue undoFunction, QScriptValue undoData, - QScriptValue redoFunction, QScriptValue redoData) : - _hasRedone(false), - _undoFunction(undoFunction), - _undoData(undoData), - _redoFunction(redoFunction), - _redoData(redoData) { -} - -void ScriptUndoCommand::undo() { - QMetaObject::invokeMethod(this, "doUndo"); -} - -void ScriptUndoCommand::redo() { - // QUndoStack will call `redo()` when adding a command to the stack. This - // makes it difficult to work with commands that span a period of time - for instance, - // the entity duplicate + move command that duplicates an entity and then moves it. - // A better implementation might be to properly implement `mergeWith()` and `id()` - // so that the two actions in the example would be merged. - if (_hasRedone) { - QMetaObject::invokeMethod(this, "doRedo"); - } - _hasRedone = true; -} - -void ScriptUndoCommand::doUndo() { - QScriptValueList args; - args << _undoData; - _undoFunction.call(QScriptValue(), args); -} - -void ScriptUndoCommand::doRedo() { - QScriptValueList args; - args << _redoData; - _redoFunction.call(QScriptValue(), args); -} diff --git a/libraries/script-engine/src/UndoStackScriptingInterface.h b/libraries/script-engine/src/UndoStackScriptingInterface.h deleted file mode 100644 index 420a282c38..0000000000 --- a/libraries/script-engine/src/UndoStackScriptingInterface.h +++ /dev/null @@ -1,53 +0,0 @@ -// -// UndoStackScriptingInterface.h -// libraries/script-engine/src -// -// Created by Ryan Huffman on 10/22/14. -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_UndoStackScriptingInterface_h -#define hifi_UndoStackScriptingInterface_h - -#include -#include -#include - -class UndoStackScriptingInterface : public QObject { - Q_OBJECT -public: - UndoStackScriptingInterface(QUndoStack* undoStack); - -public slots: - void pushCommand(QScriptValue undoFunction, QScriptValue undoData, QScriptValue redoFunction, QScriptValue redoData); - -private: - QUndoStack* _undoStack; -}; - -class ScriptUndoCommand : public QObject, public QUndoCommand { - Q_OBJECT -public: - ScriptUndoCommand(QScriptValue undoFunction, QScriptValue undoData, QScriptValue redoFunction, QScriptValue redoData); - - virtual void undo() override; - virtual void redo() override; - virtual bool mergeWith(const QUndoCommand* command) override { return false; } - virtual int id() const override { return -1; } - -public slots: - void doUndo(); - void doRedo(); - -private: - bool _hasRedone; - QScriptValue _undoFunction; - QScriptValue _undoData; - QScriptValue _redoFunction; - QScriptValue _redoData; -}; - -#endif // hifi_UndoStackScriptingInterface_h diff --git a/scripts/system/edit.js b/scripts/system/edit.js index b1f01e1ea9..27858722ec 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -850,6 +850,7 @@ var toolBar = (function () { })); isActive = active; activeButton.editProperties({isActive: isActive}); + undoHistory.setEnabled(isActive); var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); @@ -1237,6 +1238,19 @@ var originalLightsArePickable = Entities.getLightsArePickable(); function setupModelMenus() { // adj our menuitems + Menu.addMenuItem({ + menuName: "Edit", + menuItemName: "Undo", + shortcutKey: 'Ctrl+Z', + position: 0, + }); + Menu.addMenuItem({ + menuName: "Edit", + menuItemName: "Redo", + shortcutKey: 'Ctrl+Shift+Z', + position: 1, + }); + Menu.addMenuItem({ menuName: "Edit", menuItemName: "Entities", @@ -1356,6 +1370,9 @@ function setupModelMenus() { setupModelMenus(); // do this when first running our script. function cleanupModelMenus() { + Menu.removeMenuItem("Edit", "Undo"); + Menu.removeMenuItem("Edit", "Redo"); + Menu.removeSeparator("Edit", "Entities"); if (modelMenuAddedDelete) { // delete our menuitems @@ -1698,6 +1715,10 @@ function handeMenuEvent(menuItem) { Entities.setLightsArePickable(Menu.isOptionChecked("Allow Selecting of Lights")); } else if (menuItem === "Delete") { deleteSelectedEntities(); + } else if (menuItem === "Undo") { + undoHistory.undo(); + } else if (menuItem === "Redo") { + undoHistory.redo(); } else if (menuItem === "Parent Entity to Last") { parentSelectedEntities(); } else if (menuItem === "Unparent Entity") { @@ -1924,6 +1945,86 @@ function recursiveAdd(newParentID, parentData) { } } +var UndoHistory = function(onUpdate) { + this.history = []; + // The current position is the index of the last executed action in the history array. + // + // -1 0 1 2 3 <- position + // A B C D <- actions in history + // + // If our lastExecutedIndex is 1, the last executed action is B. + // If we undo, we undo B (index 1). If we redo, we redo C (index 2). + this.lastExecutedIndex = -1; + this.enabled = true; + this.onUpdate = onUpdate; +}; + +UndoHistory.prototype.pushCommand = function(undoFn, undoArgs, redoFn, redoArgs) { + if (!this.enabled) { + return; + } + // Delete any history following the last executed action. + this.history.splice(this.lastExecutedIndex + 1); + this.history.push({ + undoFn: undoFn, + undoArgs: undoArgs, + redoFn: redoFn, + redoArgs: redoArgs + }); + this.lastExecutedIndex++; + + if (this.onUpdate) { + this.onUpdate(); + } +}; +UndoHistory.prototype.setEnabled = function(enabled) { + this.enabled = enabled; + if (this.onUpdate) { + this.onUpdate(); + } +}; +UndoHistory.prototype.canUndo = function() { + return this.enabled && this.lastExecutedIndex >= 0; +}; +UndoHistory.prototype.canRedo = function() { + return this.enabled && this.lastExecutedIndex < this.history.length - 1; +}; +UndoHistory.prototype.undo = function() { + if (!this.canUndo()) { + console.warn("Cannot undo action"); + return; + } + + var command = this.history[this.lastExecutedIndex]; + command.undoFn(command.undoArgs); + this.lastExecutedIndex--; + + if (this.onUpdate) { + this.onUpdate(); + } +}; +UndoHistory.prototype.redo = function() { + if (!this.canRedo()) { + console.warn("Cannot redo action"); + return; + } + + var command = this.history[this.lastExecutedIndex + 1]; + command.redoFn(command.redoArgs); + this.lastExecutedIndex++; + + if (this.onUpdate) { + this.onUpdate(); + } +}; + +function updateUndoRedoMenuItems() { + Menu.setMenuEnabled("Edit > Undo", undoHistory.canUndo()); + Menu.setMenuEnabled("Edit > Redo", undoHistory.canRedo()); +} +var undoHistory = new UndoHistory(updateUndoRedoMenuItems); +updateUndoRedoMenuItems(); + // When an entity has been deleted we need a way to "undo" this deletion. Because it's not currently // possible to create an entity with a specific id, earlier undo commands to the deleted entity // will fail if there isn't a way to find the new entity id. @@ -2011,7 +2112,7 @@ function pushCommandForSelections(createdEntityData, deletedEntityData, doNotSav properties: currentProperties }); } - UndoStack.pushCommand(applyEntityProperties, undoData, applyEntityProperties, redoData); + undoHistory.pushCommand(applyEntityProperties, undoData, applyEntityProperties, redoData); } var ServerScriptStatusMonitor = function(entityID, statusCallback) {