senangwebs-photobooth 1.0.2 → 2.0.2
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/README.md +220 -236
- package/dist/swp.css +790 -256
- package/dist/swp.js +1 -1
- package/examples/data-attribute.html +69 -0
- package/examples/index.html +56 -51
- package/examples/studio.html +83 -0
- package/package.json +10 -6
- package/src/css/swp.css +790 -256
- package/src/js/core/Canvas.js +398 -0
- package/src/js/core/EventEmitter.js +188 -0
- package/src/js/core/History.js +250 -0
- package/src/js/core/Keyboard.js +323 -0
- package/src/js/filters/FilterManager.js +248 -0
- package/src/js/index.js +48 -0
- package/src/js/io/Clipboard.js +52 -0
- package/src/js/io/FileManager.js +150 -0
- package/src/js/layers/BlendModes.js +342 -0
- package/src/js/layers/Layer.js +415 -0
- package/src/js/layers/LayerManager.js +459 -0
- package/src/js/selection/Selection.js +167 -0
- package/src/js/swp.js +247 -761
- package/src/js/tools/BaseTool.js +264 -0
- package/src/js/tools/BrushTool.js +314 -0
- package/src/js/tools/CropTool.js +400 -0
- package/src/js/tools/EraserTool.js +155 -0
- package/src/js/tools/EyedropperTool.js +184 -0
- package/src/js/tools/FillTool.js +109 -0
- package/src/js/tools/GradientTool.js +141 -0
- package/src/js/tools/HandTool.js +51 -0
- package/src/js/tools/MarqueeTool.js +103 -0
- package/src/js/tools/MoveTool.js +465 -0
- package/src/js/tools/ShapeTool.js +285 -0
- package/src/js/tools/TextTool.js +253 -0
- package/src/js/tools/ToolManager.js +277 -0
- package/src/js/tools/ZoomTool.js +68 -0
- package/src/js/ui/ColorManager.js +71 -0
- package/src/js/ui/UI.js +1211 -0
- package/swp_preview1.png +0 -0
- package/swp_preview2.png +0 -0
- package/webpack.config.js +4 -11
- package/dist/styles.js +0 -1
- package/examples/customization.html +0 -360
- package/swp_preview.png +0 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangWebs Studio - History Manager
|
|
3
|
+
* Undo/redo system with state snapshots
|
|
4
|
+
* @version 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Events } from './EventEmitter.js';
|
|
8
|
+
|
|
9
|
+
export class History {
|
|
10
|
+
constructor(app, options = {}) {
|
|
11
|
+
this.app = app;
|
|
12
|
+
this.maxStates = options.maxStates || 50;
|
|
13
|
+
this.states = [];
|
|
14
|
+
this.currentIndex = -1;
|
|
15
|
+
this.isPerformingAction = false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Initialize history with first state
|
|
20
|
+
*/
|
|
21
|
+
init() {
|
|
22
|
+
this.clear();
|
|
23
|
+
this.pushState('Initial State');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Push a new state to history
|
|
28
|
+
* @param {string} actionName - Description of the action
|
|
29
|
+
* @param {Object} state - State snapshot (optional, will capture current if not provided)
|
|
30
|
+
*/
|
|
31
|
+
pushState(actionName, state = null) {
|
|
32
|
+
if (this.isPerformingAction) return;
|
|
33
|
+
|
|
34
|
+
// Remove any states after current index (for redo overwrite)
|
|
35
|
+
if (this.currentIndex < this.states.length - 1) {
|
|
36
|
+
this.states = this.states.slice(0, this.currentIndex + 1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Capture current state if not provided
|
|
40
|
+
const snapshot = state || this.captureState();
|
|
41
|
+
|
|
42
|
+
// Add new state
|
|
43
|
+
this.states.push({
|
|
44
|
+
name: actionName,
|
|
45
|
+
timestamp: Date.now(),
|
|
46
|
+
state: snapshot
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Limit history size
|
|
50
|
+
if (this.states.length > this.maxStates) {
|
|
51
|
+
this.states.shift();
|
|
52
|
+
} else {
|
|
53
|
+
this.currentIndex++;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.app.events.emit(Events.HISTORY_PUSH, {
|
|
57
|
+
actionName,
|
|
58
|
+
index: this.currentIndex,
|
|
59
|
+
total: this.states.length
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Capture current application state
|
|
65
|
+
* @returns {Object} State snapshot
|
|
66
|
+
*/
|
|
67
|
+
captureState() {
|
|
68
|
+
const layers = this.app.layers.getLayers().map(layer => ({
|
|
69
|
+
id: layer.id,
|
|
70
|
+
name: layer.name,
|
|
71
|
+
type: layer.type,
|
|
72
|
+
visible: layer.visible,
|
|
73
|
+
locked: layer.locked,
|
|
74
|
+
opacity: layer.opacity,
|
|
75
|
+
blendMode: layer.blendMode,
|
|
76
|
+
position: { ...layer.position },
|
|
77
|
+
imageData: layer.canvas ? layer.canvas.toDataURL() : null
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
layers,
|
|
82
|
+
activeLayerId: this.app.layers.activeLayer?.id,
|
|
83
|
+
canvasWidth: this.app.canvas.width,
|
|
84
|
+
canvasHeight: this.app.canvas.height,
|
|
85
|
+
zoom: this.app.canvas.zoom,
|
|
86
|
+
panX: this.app.canvas.panX,
|
|
87
|
+
panY: this.app.canvas.panY
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Restore a state snapshot
|
|
93
|
+
* @param {Object} snapshot - State to restore
|
|
94
|
+
*/
|
|
95
|
+
async restoreState(snapshot) {
|
|
96
|
+
this.isPerformingAction = true;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
// Clear existing layers
|
|
100
|
+
this.app.layers.clear();
|
|
101
|
+
|
|
102
|
+
// Restore canvas dimensions
|
|
103
|
+
this.app.canvas.resize(snapshot.canvasWidth, snapshot.canvasHeight);
|
|
104
|
+
this.app.canvas.zoom = snapshot.zoom;
|
|
105
|
+
this.app.canvas.panX = snapshot.panX;
|
|
106
|
+
this.app.canvas.panY = snapshot.panY;
|
|
107
|
+
|
|
108
|
+
// Restore layers
|
|
109
|
+
for (const layerData of snapshot.layers) {
|
|
110
|
+
const layer = await this.app.layers.addLayer({
|
|
111
|
+
id: layerData.id,
|
|
112
|
+
name: layerData.name,
|
|
113
|
+
type: layerData.type,
|
|
114
|
+
visible: layerData.visible,
|
|
115
|
+
locked: layerData.locked,
|
|
116
|
+
opacity: layerData.opacity,
|
|
117
|
+
blendMode: layerData.blendMode,
|
|
118
|
+
position: layerData.position
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Restore image data
|
|
122
|
+
if (layerData.imageData) {
|
|
123
|
+
await layer.loadFromDataURL(layerData.imageData);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Restore active layer
|
|
128
|
+
if (snapshot.activeLayerId) {
|
|
129
|
+
this.app.layers.setActiveLayer(snapshot.activeLayerId);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Re-render
|
|
133
|
+
this.app.canvas.render();
|
|
134
|
+
|
|
135
|
+
} finally {
|
|
136
|
+
this.isPerformingAction = false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Undo last action
|
|
142
|
+
* @returns {boolean} Success
|
|
143
|
+
*/
|
|
144
|
+
async undo() {
|
|
145
|
+
if (!this.canUndo()) return false;
|
|
146
|
+
|
|
147
|
+
this.currentIndex--;
|
|
148
|
+
const state = this.states[this.currentIndex];
|
|
149
|
+
|
|
150
|
+
await this.restoreState(state.state);
|
|
151
|
+
|
|
152
|
+
this.app.events.emit(Events.HISTORY_UNDO, {
|
|
153
|
+
actionName: state.name,
|
|
154
|
+
index: this.currentIndex
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Redo last undone action
|
|
162
|
+
* @returns {boolean} Success
|
|
163
|
+
*/
|
|
164
|
+
async redo() {
|
|
165
|
+
if (!this.canRedo()) return false;
|
|
166
|
+
|
|
167
|
+
this.currentIndex++;
|
|
168
|
+
const state = this.states[this.currentIndex];
|
|
169
|
+
|
|
170
|
+
await this.restoreState(state.state);
|
|
171
|
+
|
|
172
|
+
this.app.events.emit(Events.HISTORY_REDO, {
|
|
173
|
+
actionName: state.name,
|
|
174
|
+
index: this.currentIndex
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Check if undo is available
|
|
182
|
+
* @returns {boolean}
|
|
183
|
+
*/
|
|
184
|
+
canUndo() {
|
|
185
|
+
return this.currentIndex > 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Check if redo is available
|
|
190
|
+
* @returns {boolean}
|
|
191
|
+
*/
|
|
192
|
+
canRedo() {
|
|
193
|
+
return this.currentIndex < this.states.length - 1;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Go to specific history state
|
|
198
|
+
* @param {number} index - State index
|
|
199
|
+
*/
|
|
200
|
+
async goToState(index) {
|
|
201
|
+
if (index < 0 || index >= this.states.length) return false;
|
|
202
|
+
|
|
203
|
+
this.currentIndex = index;
|
|
204
|
+
const state = this.states[this.currentIndex];
|
|
205
|
+
|
|
206
|
+
await this.restoreState(state.state);
|
|
207
|
+
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get all history states
|
|
213
|
+
* @returns {Array} History states
|
|
214
|
+
*/
|
|
215
|
+
getStates() {
|
|
216
|
+
return this.states.map((state, index) => ({
|
|
217
|
+
index,
|
|
218
|
+
name: state.name,
|
|
219
|
+
timestamp: state.timestamp,
|
|
220
|
+
isCurrent: index === this.currentIndex
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get current state index
|
|
226
|
+
* @returns {number}
|
|
227
|
+
*/
|
|
228
|
+
getCurrentIndex() {
|
|
229
|
+
return this.currentIndex;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Clear all history
|
|
234
|
+
*/
|
|
235
|
+
clear() {
|
|
236
|
+
this.states = [];
|
|
237
|
+
this.currentIndex = -1;
|
|
238
|
+
this.app.events.emit(Events.HISTORY_CLEAR);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get history count
|
|
243
|
+
* @returns {number}
|
|
244
|
+
*/
|
|
245
|
+
get count() {
|
|
246
|
+
return this.states.length;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export default History;
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SenangWebs Studio - Keyboard Manager
|
|
3
|
+
* Keyboard shortcuts and hotkey management
|
|
4
|
+
* @version 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class Keyboard {
|
|
8
|
+
constructor(app) {
|
|
9
|
+
this.app = app;
|
|
10
|
+
this.shortcuts = new Map();
|
|
11
|
+
this.enabled = true;
|
|
12
|
+
this.modifiers = {
|
|
13
|
+
ctrl: false,
|
|
14
|
+
shift: false,
|
|
15
|
+
alt: false,
|
|
16
|
+
meta: false
|
|
17
|
+
};
|
|
18
|
+
this.spacePressed = false;
|
|
19
|
+
|
|
20
|
+
this.init();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Initialize keyboard listeners
|
|
25
|
+
*/
|
|
26
|
+
init() {
|
|
27
|
+
this.registerDefaultShortcuts();
|
|
28
|
+
|
|
29
|
+
document.addEventListener('keydown', this.handleKeyDown.bind(this));
|
|
30
|
+
document.addEventListener('keyup', this.handleKeyUp.bind(this));
|
|
31
|
+
window.addEventListener('blur', this.resetModifiers.bind(this));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Register default Photoshop-like shortcuts
|
|
36
|
+
*/
|
|
37
|
+
registerDefaultShortcuts() {
|
|
38
|
+
// Tool shortcuts
|
|
39
|
+
this.register('v', () => this.app.tools.setTool('move'));
|
|
40
|
+
this.register('m', () => this.app.tools.setTool('marquee'));
|
|
41
|
+
this.register('l', () => this.app.tools.setTool('lasso'));
|
|
42
|
+
this.register('w', () => this.app.tools.setTool('magicWand'));
|
|
43
|
+
this.register('b', () => this.app.tools.setTool('brush'));
|
|
44
|
+
this.register('e', () => this.app.tools.setTool('eraser'));
|
|
45
|
+
this.register('g', () => this.app.tools.setTool('gradient'));
|
|
46
|
+
this.register('t', () => this.app.tools.setTool('text'));
|
|
47
|
+
this.register('u', () => this.app.tools.setTool('shape'));
|
|
48
|
+
this.register('i', () => this.app.tools.setTool('eyedropper'));
|
|
49
|
+
this.register('z', () => this.app.tools.setTool('zoom'));
|
|
50
|
+
this.register('h', () => this.app.tools.setTool('hand'));
|
|
51
|
+
this.register('s', () => this.app.tools.setTool('cloneStamp'));
|
|
52
|
+
this.register('c', () => this.app.tools.setTool('crop'));
|
|
53
|
+
|
|
54
|
+
// File operations
|
|
55
|
+
this.register('ctrl+n', () => this.app.file.newDocument());
|
|
56
|
+
this.register('ctrl+o', () => this.app.file.open());
|
|
57
|
+
this.register('ctrl+s', () => this.app.file.save());
|
|
58
|
+
this.register('ctrl+shift+s', () => this.app.file.saveAs());
|
|
59
|
+
this.register('ctrl+e', () => this.app.file.export());
|
|
60
|
+
this.register('ctrl+shift+e', () => this.app.file.exportAs());
|
|
61
|
+
|
|
62
|
+
// Edit operations
|
|
63
|
+
this.register('ctrl+z', () => this.app.history.undo());
|
|
64
|
+
this.register('ctrl+shift+z', () => this.app.history.redo());
|
|
65
|
+
this.register('ctrl+y', () => this.app.history.redo());
|
|
66
|
+
this.register('ctrl+a', () => this.app.selection.selectAll());
|
|
67
|
+
this.register('ctrl+d', () => this.app.selection.deselect());
|
|
68
|
+
this.register('ctrl+shift+i', () => this.app.selection.invert());
|
|
69
|
+
this.register('ctrl+c', () => this.app.clipboard.copy());
|
|
70
|
+
this.register('ctrl+v', () => this.app.clipboard.paste());
|
|
71
|
+
this.register('ctrl+x', () => this.app.clipboard.cut());
|
|
72
|
+
this.register('ctrl+t', () => this.app.tools.startTransform());
|
|
73
|
+
this.register('delete', () => this.app.layers.deleteSelection());
|
|
74
|
+
this.register('backspace', () => this.app.layers.deleteSelection());
|
|
75
|
+
|
|
76
|
+
// View operations
|
|
77
|
+
this.register('ctrl+0', () => this.app.canvas.fitToScreen());
|
|
78
|
+
this.register('ctrl+1', () => this.app.canvas.setZoom(100));
|
|
79
|
+
this.register('ctrl+plus', () => this.app.canvas.zoomIn());
|
|
80
|
+
this.register('ctrl+minus', () => this.app.canvas.zoomOut());
|
|
81
|
+
this.register('tab', () => this.app.ui.togglePanels());
|
|
82
|
+
this.register('f', () => this.app.ui.toggleFullscreen());
|
|
83
|
+
|
|
84
|
+
// Brush size
|
|
85
|
+
this.register('[', () => this.app.tools.decreaseBrushSize());
|
|
86
|
+
this.register(']', () => this.app.tools.increaseBrushSize());
|
|
87
|
+
this.register('shift+[', () => this.app.tools.decreaseBrushHardness());
|
|
88
|
+
this.register('shift+]', () => this.app.tools.increaseBrushHardness());
|
|
89
|
+
|
|
90
|
+
// Layer operations
|
|
91
|
+
this.register('ctrl+shift+n', () => this.app.layers.addLayer());
|
|
92
|
+
this.register('ctrl+j', () => this.app.layers.duplicateLayer());
|
|
93
|
+
this.register('ctrl+shift+e', () => this.app.layers.mergeVisible());
|
|
94
|
+
this.register('ctrl+e', () => this.app.layers.mergeDown());
|
|
95
|
+
|
|
96
|
+
// Color
|
|
97
|
+
this.register('x', () => this.app.colors.swap());
|
|
98
|
+
this.register('d', () => this.app.colors.reset());
|
|
99
|
+
|
|
100
|
+
// Escape
|
|
101
|
+
this.register('escape', () => this.app.cancelCurrentAction());
|
|
102
|
+
this.register('enter', () => this.app.confirmCurrentAction());
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Register a keyboard shortcut
|
|
107
|
+
* @param {string} shortcut - Shortcut string (e.g., 'ctrl+s', 'shift+a')
|
|
108
|
+
* @param {Function} callback - Callback function
|
|
109
|
+
* @param {Object} options - Options
|
|
110
|
+
*/
|
|
111
|
+
register(shortcut, callback, options = {}) {
|
|
112
|
+
const key = this.normalizeShortcut(shortcut);
|
|
113
|
+
this.shortcuts.set(key, {
|
|
114
|
+
callback,
|
|
115
|
+
preventDefault: options.preventDefault !== false,
|
|
116
|
+
allowInInput: options.allowInInput || false,
|
|
117
|
+
description: options.description || ''
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Unregister a keyboard shortcut
|
|
123
|
+
* @param {string} shortcut - Shortcut string
|
|
124
|
+
*/
|
|
125
|
+
unregister(shortcut) {
|
|
126
|
+
const key = this.normalizeShortcut(shortcut);
|
|
127
|
+
this.shortcuts.delete(key);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Normalize shortcut string
|
|
132
|
+
* @param {string} shortcut - Shortcut string
|
|
133
|
+
* @returns {string} Normalized shortcut
|
|
134
|
+
*/
|
|
135
|
+
normalizeShortcut(shortcut) {
|
|
136
|
+
return shortcut
|
|
137
|
+
.toLowerCase()
|
|
138
|
+
.split('+')
|
|
139
|
+
.sort((a, b) => {
|
|
140
|
+
const order = ['ctrl', 'alt', 'shift', 'meta'];
|
|
141
|
+
const aIndex = order.indexOf(a);
|
|
142
|
+
const bIndex = order.indexOf(b);
|
|
143
|
+
if (aIndex === -1 && bIndex === -1) return a.localeCompare(b);
|
|
144
|
+
if (aIndex === -1) return 1;
|
|
145
|
+
if (bIndex === -1) return -1;
|
|
146
|
+
return aIndex - bIndex;
|
|
147
|
+
})
|
|
148
|
+
.join('+');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Build shortcut string from event
|
|
153
|
+
* @param {KeyboardEvent} e - Keyboard event
|
|
154
|
+
* @returns {string} Shortcut string
|
|
155
|
+
*/
|
|
156
|
+
buildShortcutFromEvent(e) {
|
|
157
|
+
const parts = [];
|
|
158
|
+
|
|
159
|
+
if (e.ctrlKey || e.metaKey) parts.push('ctrl');
|
|
160
|
+
if (e.altKey) parts.push('alt');
|
|
161
|
+
if (e.shiftKey) parts.push('shift');
|
|
162
|
+
|
|
163
|
+
let key = e.key.toLowerCase();
|
|
164
|
+
|
|
165
|
+
// Normalize special keys
|
|
166
|
+
const keyMap = {
|
|
167
|
+
' ': 'space',
|
|
168
|
+
'arrowup': 'up',
|
|
169
|
+
'arrowdown': 'down',
|
|
170
|
+
'arrowleft': 'left',
|
|
171
|
+
'arrowright': 'right',
|
|
172
|
+
'=': 'plus',
|
|
173
|
+
'-': 'minus'
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
key = keyMap[key] || key;
|
|
177
|
+
|
|
178
|
+
// Don't add modifier keys as the main key
|
|
179
|
+
if (!['control', 'alt', 'shift', 'meta'].includes(key)) {
|
|
180
|
+
parts.push(key);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return this.normalizeShortcut(parts.join('+'));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Handle keydown event
|
|
188
|
+
* @param {KeyboardEvent} e - Keyboard event
|
|
189
|
+
*/
|
|
190
|
+
handleKeyDown(e) {
|
|
191
|
+
// Update modifier state
|
|
192
|
+
this.modifiers.ctrl = e.ctrlKey || e.metaKey;
|
|
193
|
+
this.modifiers.shift = e.shiftKey;
|
|
194
|
+
this.modifiers.alt = e.altKey;
|
|
195
|
+
this.modifiers.meta = e.metaKey;
|
|
196
|
+
|
|
197
|
+
// Handle space for temporary hand tool
|
|
198
|
+
if (e.code === 'Space' && !this.spacePressed) {
|
|
199
|
+
// Don't activate hand tool if text tool is editing
|
|
200
|
+
const currentTool = this.app.tools?.currentTool;
|
|
201
|
+
if (currentTool?.name === 'text' && currentTool?.editingLayer) {
|
|
202
|
+
return; // Allow space in text
|
|
203
|
+
}
|
|
204
|
+
this.spacePressed = true;
|
|
205
|
+
this.app.tools.activateTemporaryTool('hand');
|
|
206
|
+
e.preventDefault();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!this.enabled) return;
|
|
211
|
+
|
|
212
|
+
// Check if text tool is actively editing - allow all typing
|
|
213
|
+
const currentTool = this.app.tools?.currentTool;
|
|
214
|
+
if (currentTool?.name === 'text' && currentTool?.editingLayer) {
|
|
215
|
+
// Only process shortcuts with modifiers (Ctrl, Alt, Meta) during text editing
|
|
216
|
+
if (!e.ctrlKey && !e.altKey && !e.metaKey) {
|
|
217
|
+
// Allow normal typing, don't process tool shortcuts
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Check if in input field
|
|
223
|
+
const target = e.target;
|
|
224
|
+
const isInputField = target.tagName === 'INPUT' ||
|
|
225
|
+
target.tagName === 'TEXTAREA' ||
|
|
226
|
+
target.contentEditable === 'true';
|
|
227
|
+
|
|
228
|
+
// Build shortcut string
|
|
229
|
+
const shortcut = this.buildShortcutFromEvent(e);
|
|
230
|
+
|
|
231
|
+
// Look up shortcut
|
|
232
|
+
const handler = this.shortcuts.get(shortcut);
|
|
233
|
+
|
|
234
|
+
if (handler) {
|
|
235
|
+
// Skip if in input field and not allowed
|
|
236
|
+
if (isInputField && !handler.allowInInput) return;
|
|
237
|
+
|
|
238
|
+
if (handler.preventDefault) {
|
|
239
|
+
e.preventDefault();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
handler.callback(e);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
console.error(`Error executing shortcut "${shortcut}":`, error);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Handle keyup event
|
|
252
|
+
* @param {KeyboardEvent} e - Keyboard event
|
|
253
|
+
*/
|
|
254
|
+
handleKeyUp(e) {
|
|
255
|
+
// Update modifier state
|
|
256
|
+
this.modifiers.ctrl = e.ctrlKey || e.metaKey;
|
|
257
|
+
this.modifiers.shift = e.shiftKey;
|
|
258
|
+
this.modifiers.alt = e.altKey;
|
|
259
|
+
this.modifiers.meta = e.metaKey;
|
|
260
|
+
|
|
261
|
+
// Handle space release
|
|
262
|
+
if (e.code === 'Space') {
|
|
263
|
+
this.spacePressed = false;
|
|
264
|
+
this.app.tools.deactivateTemporaryTool();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Reset modifier states
|
|
270
|
+
*/
|
|
271
|
+
resetModifiers() {
|
|
272
|
+
this.modifiers.ctrl = false;
|
|
273
|
+
this.modifiers.shift = false;
|
|
274
|
+
this.modifiers.alt = false;
|
|
275
|
+
this.modifiers.meta = false;
|
|
276
|
+
this.spacePressed = false;
|
|
277
|
+
this.app.tools.deactivateTemporaryTool();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Enable/disable keyboard shortcuts
|
|
282
|
+
* @param {boolean} enabled - Enable state
|
|
283
|
+
*/
|
|
284
|
+
setEnabled(enabled) {
|
|
285
|
+
this.enabled = enabled;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Check if a modifier is pressed
|
|
290
|
+
* @param {string} modifier - Modifier name
|
|
291
|
+
* @returns {boolean}
|
|
292
|
+
*/
|
|
293
|
+
isModifierPressed(modifier) {
|
|
294
|
+
return this.modifiers[modifier] || false;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get all registered shortcuts
|
|
299
|
+
* @returns {Array} Shortcuts with descriptions
|
|
300
|
+
*/
|
|
301
|
+
getShortcuts() {
|
|
302
|
+
const shortcuts = [];
|
|
303
|
+
this.shortcuts.forEach((handler, key) => {
|
|
304
|
+
shortcuts.push({
|
|
305
|
+
shortcut: key,
|
|
306
|
+
description: handler.description
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
return shortcuts;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Destroy keyboard manager
|
|
314
|
+
*/
|
|
315
|
+
destroy() {
|
|
316
|
+
document.removeEventListener('keydown', this.handleKeyDown);
|
|
317
|
+
document.removeEventListener('keyup', this.handleKeyUp);
|
|
318
|
+
window.removeEventListener('blur', this.resetModifiers);
|
|
319
|
+
this.shortcuts.clear();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export default Keyboard;
|