@quenty/undostack 1.0.1-canary.303.3e5dce0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/LICENSE.md +21 -0
- package/README.md +21 -0
- package/default.project.json +6 -0
- package/package.json +37 -0
- package/src/Shared/UndoStack.lua +238 -0
- package/src/Shared/UndoStackEntry.lua +69 -0
- package/src/node_modules.project.json +7 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
+
|
|
6
|
+
## 1.0.1-canary.303.3e5dce0.0 (2022-11-08)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* Add baseline undo stack system ([ddda496](https://github.com/Quenty/NevermoreEngine/commit/ddda496b12e0f492258e7a83e65d291f3cae337c))
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2014-2022 Quenty
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
## UndoStack
|
|
2
|
+
<div align="center">
|
|
3
|
+
<a href="http://quenty.github.io/NevermoreEngine/">
|
|
4
|
+
<img src="https://github.com/Quenty/NevermoreEngine/actions/workflows/docs.yml/badge.svg" alt="Documentation status" />
|
|
5
|
+
</a>
|
|
6
|
+
<a href="https://discord.gg/mhtGUS8">
|
|
7
|
+
<img src="https://img.shields.io/discord/385151591524597761?color=5865F2&label=discord&logo=discord&logoColor=white" alt="Discord" />
|
|
8
|
+
</a>
|
|
9
|
+
<a href="https://github.com/Quenty/NevermoreEngine/actions">
|
|
10
|
+
<img src="https://github.com/Quenty/NevermoreEngine/actions/workflows/build.yml/badge.svg" alt="Build and release status" />
|
|
11
|
+
</a>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
Undo stack functionality for editing stuff
|
|
15
|
+
|
|
16
|
+
<div align="center"><a href="https://quenty.github.io/NevermoreEngine/api/undostack">View docs →</a></div>
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
```
|
|
20
|
+
npm install @quenty/undostack --save
|
|
21
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quenty/undostack",
|
|
3
|
+
"version": "1.0.1-canary.303.3e5dce0.0",
|
|
4
|
+
"description": "Generalized undo stack for Roblox",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"Roblox",
|
|
7
|
+
"Nevermore",
|
|
8
|
+
"Lua",
|
|
9
|
+
"Undo"
|
|
10
|
+
],
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Quenty/NevermoreEngine/issues"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/Quenty/NevermoreEngine.git",
|
|
17
|
+
"directory": "src/undostack/"
|
|
18
|
+
},
|
|
19
|
+
"funding": {
|
|
20
|
+
"type": "patreon",
|
|
21
|
+
"url": "https://www.patreon.com/quenty"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"contributors": [
|
|
25
|
+
"Quenty"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@quenty/baseobject": "6.0.1",
|
|
29
|
+
"@quenty/instanceutils": "7.1.1",
|
|
30
|
+
"@quenty/loader": "6.0.1",
|
|
31
|
+
"@quenty/promise": "6.0.1"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"gitHead": "3e5dce0b249a270d35586e5897a36e84611e7834"
|
|
37
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
@class UndoStack
|
|
3
|
+
]=]
|
|
4
|
+
|
|
5
|
+
local require = require(script.Parent.loader).load(script)
|
|
6
|
+
|
|
7
|
+
local BaseObject = require("BaseObject")
|
|
8
|
+
local Promise = require("Promise")
|
|
9
|
+
local UndoStackEntry = require("UndoStackEntry")
|
|
10
|
+
local RxInstanceUtils = require("RxInstanceUtils")
|
|
11
|
+
|
|
12
|
+
local DEFAULT_MAX_SIZE = 25
|
|
13
|
+
|
|
14
|
+
local UndoStack = setmetatable({}, BaseObject)
|
|
15
|
+
UndoStack.ClassName = "UndoStack"
|
|
16
|
+
UndoStack.__index = UndoStack
|
|
17
|
+
|
|
18
|
+
function UndoStack.new(maxSize)
|
|
19
|
+
local self = setmetatable(BaseObject.new(), UndoStack)
|
|
20
|
+
|
|
21
|
+
assert(type(maxSize) == "number" or maxSize == nil, "Bad maxSize")
|
|
22
|
+
|
|
23
|
+
self._maxSize = maxSize or DEFAULT_MAX_SIZE
|
|
24
|
+
|
|
25
|
+
self._undoStack = {}
|
|
26
|
+
self._redoStack = {}
|
|
27
|
+
|
|
28
|
+
self._hasUndoEntries = Instance.new("BoolValue")
|
|
29
|
+
self._hasUndoEntries.Value = false
|
|
30
|
+
self._maid:GiveTask(self._hasUndoEntries)
|
|
31
|
+
|
|
32
|
+
self._hasRedoEntries = Instance.new("BoolValue")
|
|
33
|
+
self._hasRedoEntries.Value = false
|
|
34
|
+
self._maid:GiveTask(self._hasRedoEntries)
|
|
35
|
+
|
|
36
|
+
self._isActionExecuting = Instance.new("BoolValue")
|
|
37
|
+
self._isActionExecuting.Value = false
|
|
38
|
+
self._maid:GiveTask(self._isActionExecuting)
|
|
39
|
+
|
|
40
|
+
return self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
--[=[
|
|
44
|
+
Clears the redo stack manually. This may be required if you do an action but
|
|
45
|
+
can't push an undo.
|
|
46
|
+
@return boolean
|
|
47
|
+
]=]
|
|
48
|
+
function UndoStack:ClearRedoStack()
|
|
49
|
+
self._redoStack ={}
|
|
50
|
+
self:_updateHasRedoEntries()
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
--[=[
|
|
54
|
+
Returns true if an action is executing
|
|
55
|
+
@return boolean
|
|
56
|
+
]=]
|
|
57
|
+
function UndoStack:IsActionExecuting()
|
|
58
|
+
return self._isActionExecuting.Value
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
--[=[
|
|
62
|
+
Observes whether the stack has undo entries
|
|
63
|
+
@return Observable<boolean>
|
|
64
|
+
]=]
|
|
65
|
+
function UndoStack:ObserveHasUndoEntries()
|
|
66
|
+
return RxInstanceUtils.observeProperty(self._hasUndoEntries, "Value")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
--[=[
|
|
70
|
+
Observes whether the stack has redo entries
|
|
71
|
+
@return Observable<boolean>
|
|
72
|
+
]=]
|
|
73
|
+
function UndoStack:ObserveHasRedoEntries()
|
|
74
|
+
return RxInstanceUtils.observeProperty(self._hasRedoEntries, "Value")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
--[=[
|
|
78
|
+
Returns true if there are undo entries on the stack
|
|
79
|
+
@return boolean
|
|
80
|
+
]=]
|
|
81
|
+
function UndoStack:HasUndoEntries()
|
|
82
|
+
return self._hasUndoEntries.Value
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
--[=[
|
|
86
|
+
Returns true if there are redo entries on the stack
|
|
87
|
+
@return boolean
|
|
88
|
+
]=]
|
|
89
|
+
function UndoStack:HasRedoEntries()
|
|
90
|
+
return self._hasRedoEntries.Value
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
--[=[
|
|
94
|
+
Pushes an action to be undone.
|
|
95
|
+
|
|
96
|
+
```lua
|
|
97
|
+
local entry = UndoStackEntry.new()
|
|
98
|
+
entry:SetPromiseUndo(function()
|
|
99
|
+
return buildService:PromiseSellItem(item)
|
|
100
|
+
end)
|
|
101
|
+
entry:SetPromiseRedo(function()
|
|
102
|
+
return buildService:PromisePlaceItem(item)
|
|
103
|
+
end)
|
|
104
|
+
|
|
105
|
+
maid:GiveTask(undoStack:Push(entry))
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
@param undoStackEntry UndoStackEntry
|
|
109
|
+
@return function -- Callback that removes the action
|
|
110
|
+
]=]
|
|
111
|
+
function UndoStack:Push(undoStackEntry)
|
|
112
|
+
assert(UndoStackEntry.isUndoStackEntry(undoStackEntry), "Bad undoStackEntry")
|
|
113
|
+
|
|
114
|
+
table.insert(self._undoStack, undoStackEntry)
|
|
115
|
+
while #self._undoStack > self._maxSize do
|
|
116
|
+
table.remove(self._undoStack, 1)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
self._redoStack = {}
|
|
120
|
+
|
|
121
|
+
self:_updateHasUndoEntries()
|
|
122
|
+
self:_updateHasRedoEntries()
|
|
123
|
+
|
|
124
|
+
return function()
|
|
125
|
+
self:Remove(undoStackEntry)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
--[=[
|
|
130
|
+
Removes the action specified from the stack entirely. If the action was queued to run,
|
|
131
|
+
it may still run.
|
|
132
|
+
|
|
133
|
+
@param undoStackEntry The undo stack entry to remove
|
|
134
|
+
]=]
|
|
135
|
+
function UndoStack:Remove(undoStackEntry)
|
|
136
|
+
assert(UndoStackEntry.isUndoStackEntry(undoStackEntry), "Bad undoStackEntry")
|
|
137
|
+
|
|
138
|
+
local undoIndex = table.find(self._undoStack, undoStackEntry)
|
|
139
|
+
if undoIndex then
|
|
140
|
+
table.remove(self._undoStack, undoIndex)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
local redoIndex = table.find(self._redoStack, undoStackEntry)
|
|
144
|
+
if redoIndex then
|
|
145
|
+
table.remove(self._redoStack, redoIndex)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
self:_updateHasUndoEntries()
|
|
149
|
+
self:_updateHasRedoEntries()
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
--[=[
|
|
153
|
+
Undoes from the stack. If a current action is going on, it will finish running.
|
|
154
|
+
|
|
155
|
+
@return Promise
|
|
156
|
+
]=]
|
|
157
|
+
function UndoStack:PromiseUndo()
|
|
158
|
+
return self:_promiseCurrent(function()
|
|
159
|
+
local undoStackEntry = table.remove(self._undoStack)
|
|
160
|
+
if not undoStackEntry then
|
|
161
|
+
return Promise.resolved(false)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
self:_updateHasUndoEntries()
|
|
165
|
+
|
|
166
|
+
return undoStackEntry:PromiseUndo()
|
|
167
|
+
:Then(function()
|
|
168
|
+
if undoStackEntry:HasRedo() then
|
|
169
|
+
table.insert(self._redoStack, undoStackEntry)
|
|
170
|
+
self:_updateHasRedoEntries()
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
return true
|
|
174
|
+
end)
|
|
175
|
+
end)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
--[=[
|
|
179
|
+
Redoes the from the stack. If a current action is going on, it will be queued.
|
|
180
|
+
|
|
181
|
+
@return Promise
|
|
182
|
+
]=]
|
|
183
|
+
function UndoStack:PromiseRedo()
|
|
184
|
+
return self:_promiseCurrent(function()
|
|
185
|
+
local undoStackEntry = table.remove(self._redoStack)
|
|
186
|
+
if not undoStackEntry then
|
|
187
|
+
return Promise.resolved(false)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
self:_updateHasRedoEntries()
|
|
191
|
+
|
|
192
|
+
return undoStackEntry:PromiseRedo()
|
|
193
|
+
:Then(function()
|
|
194
|
+
if undoStackEntry:HasUndo() then
|
|
195
|
+
table.insert(self._undoStack, undoStackEntry)
|
|
196
|
+
self:_updateHasUndoEntries()
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
return true
|
|
200
|
+
end)
|
|
201
|
+
end)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
function UndoStack:_promiseCurrent(doNextPromise)
|
|
205
|
+
local promise
|
|
206
|
+
if self._latestPromiseChain then
|
|
207
|
+
promise = self._latestPromiseChain
|
|
208
|
+
:Finally(function()
|
|
209
|
+
return self._maid:GivePromise(doNextPromise())
|
|
210
|
+
end)
|
|
211
|
+
else
|
|
212
|
+
promise = self._maid:GivePromise(doNextPromise())
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
self._latestPromiseChain = promise
|
|
216
|
+
|
|
217
|
+
self._isActionExecuting.Value = true
|
|
218
|
+
|
|
219
|
+
-- Clean out the actual latest promise
|
|
220
|
+
promise:Finally(function()
|
|
221
|
+
if self._latestPromiseChain == promise then
|
|
222
|
+
self._latestPromiseChain = nil
|
|
223
|
+
self._isActionExecuting.Value = false
|
|
224
|
+
end
|
|
225
|
+
end)
|
|
226
|
+
|
|
227
|
+
return promise
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
function UndoStack:_updateHasUndoEntries()
|
|
231
|
+
self._hasUndoEntries.Value = #self._undoStack > 0
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
function UndoStack:_updateHasRedoEntries()
|
|
235
|
+
self._hasRedoEntries.Value = #self._redoStack > 0
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
return UndoStack
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
@class UndoStackEntry
|
|
3
|
+
]=]
|
|
4
|
+
|
|
5
|
+
local require = require(script.Parent.loader).load(script)
|
|
6
|
+
|
|
7
|
+
local Promise = require("Promise")
|
|
8
|
+
|
|
9
|
+
local UndoStackEntry = {}
|
|
10
|
+
UndoStackEntry.ClassName = "UndoStackEntry"
|
|
11
|
+
UndoStackEntry.__index = UndoStackEntry
|
|
12
|
+
|
|
13
|
+
function UndoStackEntry.new()
|
|
14
|
+
local self = setmetatable({}, UndoStackEntry)
|
|
15
|
+
|
|
16
|
+
return self
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
function UndoStackEntry.isUndoStackEntry(value)
|
|
20
|
+
return type(value) == "table" and getmetatable(value) == UndoStackEntry
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
function UndoStackEntry:SetPromiseUndo(promiseUndo)
|
|
24
|
+
assert(type(promiseUndo) == "function" or promiseUndo == nil, "Bad promiseUndo")
|
|
25
|
+
|
|
26
|
+
self._promiseUndo = promiseUndo
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
function UndoStackEntry:SetPromiseRedo(promiseRedo)
|
|
30
|
+
assert(type(promiseRedo) == "function" or promiseRedo == nil, "Bad promiseRedo")
|
|
31
|
+
|
|
32
|
+
self._promiseRedo = promiseRedo
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
function UndoStackEntry:HasUndo()
|
|
36
|
+
return self._promiseUndo ~= nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
function UndoStackEntry:HasRedo()
|
|
40
|
+
return self._promiseRedo ~= nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
function UndoStackEntry:PromiseUndo()
|
|
44
|
+
if not self._promiseUndo then
|
|
45
|
+
return Promise.resolved()
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
local result = self._promiseUndo()
|
|
49
|
+
if Promise.isPromise(result) then
|
|
50
|
+
return result
|
|
51
|
+
else
|
|
52
|
+
return Promise.resolved(result)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
function UndoStackEntry:PromiseRedo()
|
|
57
|
+
if not self._promiseUndo then
|
|
58
|
+
return Promise.resolved()
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
local result = self._promiseRedo()
|
|
62
|
+
if Promise.isPromise(result) then
|
|
63
|
+
return result
|
|
64
|
+
else
|
|
65
|
+
return Promise.resolved(result)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
return UndoStackEntry
|