gopeak 2.1.0 → 2.2.1

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.
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Context menu, new script modal, and view switching
3
+ */
4
+ import { nodes, edges, setCurrentView, setSceneData, getFolderColor, setExpandedScene, setExpandedSceneHierarchy, setSelectedSceneNode, setHoveredSceneNode, expandedScene, gitChangeSummary, categoryColorMap } from './state.js';
5
+ import { sendCommand } from './websocket.js';
6
+ import { draw, getCanvas, roundRect, getContext, clearPositions, fitToView } from './canvas.js';
7
+ import { initLayout } from './layout.js';
8
+ import { closePanel, closeSceneNodePanel } from './panel.js';
9
+ import { updateStats, buildChangesPanel } from './events.js';
10
+ let contextMenu;
11
+ export function initModals() {
12
+ contextMenu = document.getElementById('context-menu');
13
+ initContextMenu();
14
+ }
15
+ // ---- Context Menu ----
16
+ function initContextMenu() {
17
+ const canvas = getCanvas();
18
+ canvas.addEventListener('contextmenu', (e) => {
19
+ e.preventDefault();
20
+ // Position menu at mouse
21
+ contextMenu.style.left = e.clientX + 'px';
22
+ contextMenu.style.top = e.clientY + 'px';
23
+ contextMenu.classList.add('visible');
24
+ });
25
+ // Hide context menu on click elsewhere
26
+ document.addEventListener('click', (e) => {
27
+ if (!contextMenu.contains(e.target)) {
28
+ contextMenu.classList.remove('visible');
29
+ }
30
+ });
31
+ document.addEventListener('keydown', (e) => {
32
+ if (e.key === 'Escape') {
33
+ contextMenu.classList.remove('visible');
34
+ closeNewScriptModal();
35
+ }
36
+ });
37
+ }
38
+ // ---- New Script Creation ----
39
+ window.createNewScript = function () {
40
+ contextMenu.classList.remove('visible');
41
+ document.getElementById('new-script-modal').style.display = 'flex';
42
+ document.getElementById('new-script-path').focus();
43
+ };
44
+ window.closeNewScriptModal = function () {
45
+ document.getElementById('new-script-modal').style.display = 'none';
46
+ };
47
+ function closeNewScriptModal() {
48
+ document.getElementById('new-script-modal').style.display = 'none';
49
+ }
50
+ window.submitNewScript = async function () {
51
+ const path = document.getElementById('new-script-path').value.trim();
52
+ const extendsType = document.getElementById('new-script-extends').value;
53
+ const className = document.getElementById('new-script-classname').value.trim();
54
+ if (!path) {
55
+ alert('Please enter a script path');
56
+ return;
57
+ }
58
+ if (!path.startsWith('res://') || !path.endsWith('.gd')) {
59
+ alert('Path must start with res:// and end with .gd');
60
+ return;
61
+ }
62
+ try {
63
+ // Use existing create_script tool via invokeTool (not internal)
64
+ const result = await sendCommand('create_script_file', {
65
+ path: path,
66
+ extends: extendsType,
67
+ class_name: className || ''
68
+ });
69
+ if (result.ok) {
70
+ closeNewScriptModal();
71
+ // Refresh the project map
72
+ refreshProject();
73
+ }
74
+ else {
75
+ alert('Failed to create script: ' + (result.error || 'Unknown error'));
76
+ }
77
+ }
78
+ catch (err) {
79
+ alert('Failed to create script: ' + err.message);
80
+ }
81
+ };
82
+ window.refreshProject = async function () {
83
+ contextMenu.classList.remove('visible');
84
+ const refreshBtn = document.getElementById('refresh-btn');
85
+ if (!refreshBtn)
86
+ return;
87
+ // Prevent double-clicks
88
+ if (refreshBtn.classList.contains('spinning'))
89
+ return;
90
+ refreshBtn.classList.add('spinning');
91
+ const spinStart = Date.now();
92
+ let success = false;
93
+ try {
94
+ const result = await sendCommand('refresh_map', {});
95
+ if (result.ok && result.project_map) {
96
+ // Build old-position lookup by path for stable repositioning
97
+ const oldPositions = {};
98
+ nodes.forEach(n => { oldPositions[n.path] = { x: n.x, y: n.y }; });
99
+ // Map new nodes, preserving positions for existing paths
100
+ const newNodes = result.project_map.nodes.map((n) => {
101
+ const old = oldPositions[n.path];
102
+ return {
103
+ ...n,
104
+ x: old ? old.x : 0,
105
+ y: old ? old.y : 0,
106
+ color: categoryColorMap[n.category] || getFolderColor(n.folder),
107
+ highlighted: true,
108
+ visible: true,
109
+ categoryVisible: true
110
+ };
111
+ });
112
+ nodes.length = 0;
113
+ nodes.push(...newNodes);
114
+ edges.length = 0;
115
+ edges.push(...result.project_map.edges);
116
+ // Recalculate git change summary
117
+ gitChangeSummary.modified = 0;
118
+ gitChangeSummary.added = 0;
119
+ gitChangeSummary.untracked = 0;
120
+ nodes.forEach(n => {
121
+ if (n.gitStatus === 'modified')
122
+ gitChangeSummary.modified++;
123
+ else if (n.gitStatus === 'added')
124
+ gitChangeSummary.added++;
125
+ else if (n.gitStatus === 'untracked')
126
+ gitChangeSummary.untracked++;
127
+ });
128
+ // Rebuild changes panel UI
129
+ buildChangesPanel();
130
+ // Show/hide changes panel based on whether changes exist
131
+ const totalChanges = gitChangeSummary.modified + gitChangeSummary.added + gitChangeSummary.untracked;
132
+ const changesPanel = document.getElementById('changes-panel');
133
+ if (changesPanel) {
134
+ changesPanel.style.display = totalChanges > 0 ? '' : 'none';
135
+ }
136
+ // Only run full layout for brand-new nodes (no prior position)
137
+ const hasNewNodes = newNodes.some(n => n.x === 0 && n.y === 0 && !oldPositions[n.path]);
138
+ if (hasNewNodes) {
139
+ initLayout();
140
+ }
141
+ updateStats();
142
+ draw();
143
+ success = true;
144
+ }
145
+ }
146
+ catch (err) {
147
+ console.error('Failed to refresh:', err);
148
+ }
149
+ finally {
150
+ // Ensure spinner is visible for at least 600ms so user sees feedback
151
+ const elapsed = Date.now() - spinStart;
152
+ const remaining = Math.max(0, 600 - elapsed);
153
+ setTimeout(() => {
154
+ refreshBtn.classList.remove('spinning');
155
+ // Flash success (green) or error (red) for 1.5s
156
+ const feedbackClass = success ? 'refresh-done' : 'refresh-error';
157
+ refreshBtn.classList.add(feedbackClass);
158
+ setTimeout(() => refreshBtn.classList.remove(feedbackClass), 1500);
159
+ }, remaining);
160
+ }
161
+ };
162
+ function refreshProject() {
163
+ window.refreshProject();
164
+ }
165
+ // ---- Reset Layout ----
166
+ window.resetLayout = function () {
167
+ contextMenu.classList.remove('visible');
168
+ // Clear saved positions
169
+ clearPositions();
170
+ // Re-run force-directed layout
171
+ initLayout();
172
+ // Fit view to show all nodes
173
+ fitToView(nodes);
174
+ // Redraw
175
+ draw();
176
+ };
177
+ // ---- View Switching (Scripts/Scenes) ----
178
+ window.switchView = function (view) {
179
+ const currentViewTab = document.querySelector('#view-tabs button.active')?.textContent.toLowerCase();
180
+ if (view === currentViewTab)
181
+ return;
182
+ // Close any open panels
183
+ if (view === 'scripts') {
184
+ closeSceneNodePanel();
185
+ // Clear scene state
186
+ setExpandedScene(null);
187
+ setExpandedSceneHierarchy(null);
188
+ setSelectedSceneNode(null);
189
+ setHoveredSceneNode(null);
190
+ // Hide scene back button
191
+ const backBtn = document.getElementById('scene-back-btn');
192
+ if (backBtn)
193
+ backBtn.style.display = 'none';
194
+ // Show legend for scripts view
195
+ const legend = document.getElementById('legend');
196
+ if (legend)
197
+ legend.classList.remove('hidden');
198
+ }
199
+ else {
200
+ closePanel();
201
+ }
202
+ setCurrentView(view);
203
+ // Update tab buttons
204
+ document.querySelectorAll('#view-tabs button').forEach(btn => {
205
+ btn.classList.toggle('active', btn.textContent.toLowerCase() === view);
206
+ });
207
+ // Update search placeholder
208
+ const searchInput = document.getElementById('search');
209
+ if (searchInput) {
210
+ searchInput.placeholder = view === 'scripts' ? 'Search scripts...' : 'Search scenes...';
211
+ }
212
+ if (view === 'scenes') {
213
+ loadSceneView();
214
+ }
215
+ else {
216
+ updateStats();
217
+ draw();
218
+ }
219
+ };
220
+ async function loadSceneView() {
221
+ // Request scene data from Godot
222
+ try {
223
+ const result = await sendCommand('map_scenes', { root: 'res://' });
224
+ if (result.ok) {
225
+ setSceneData(result.scene_map);
226
+ updateStats();
227
+ draw();
228
+ }
229
+ else {
230
+ console.error('Failed to load scenes:', result.error);
231
+ alert('Failed to load scenes: ' + (result.error || 'Unknown error'));
232
+ }
233
+ }
234
+ catch (err) {
235
+ console.error('Failed to load scenes:', err);
236
+ // Show placeholder for now
237
+ draw();
238
+ }
239
+ }