dazscript-framework 0.2.5 → 0.3.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.
@@ -2,52 +2,195 @@ import { debug } from '@dsf/common/log'
2
2
  import { CustomAction } from '@dsf/core/custom-action'
3
3
  import { mainWindow } from '@dsf/core/global'
4
4
  import * as array from '@dsf/helpers/array-helper'
5
- import { getMenu } from './menu-helper'
6
- import { keys } from './object-helper'
7
- import { progress } from './progress-helper'
5
+ import { keys } from '@dsf/helpers/object-helper'
6
+ import { progress } from '@dsf/helpers/progress-helper'
8
7
  import { getScriptPath } from './script-helper'
8
+ import { getMenu } from './menu-helper'
9
9
 
10
10
  const actionMgr = mainWindow.getActionMgr()
11
11
  const paneMgr = mainWindow.getPaneMgr()
12
12
 
13
- export const findByFilePath = (action: CustomAction, filePath: string): CustomAction | null => {
14
- var actionsCount = actionMgr.getNumCustomActions()
15
- for (var i = 0; i < actionsCount; i++) {
16
- var actionFilePath = actionMgr.getCustomActionFile(i)
13
+ const normalizePath = (value: string | null | undefined): string => {
14
+ if (!value) return ''
15
+
16
+ return String(value)
17
+ .replace(/\\/g, '/')
18
+ .replace(/\/+/g, '/')
19
+ .replace(/\/$/, '')
20
+ .toLowerCase()
21
+ }
22
+
23
+ const getFileName = (value: string | null | undefined): string => {
24
+ const normalized = normalizePath(value)
25
+ if (!normalized) return ''
26
+ const parts = normalized.split('/')
27
+ return parts[parts.length - 1] ?? ''
28
+ }
17
29
 
18
- if (actionFilePath != filePath) continue
30
+ const getTrailingPathSegments = (value: string | null | undefined, segmentCount: number): string => {
31
+ const normalized = normalizePath(value)
32
+ if (!normalized) return ''
19
33
 
20
- var name = actionMgr.getCustomActionName(i)
21
- var text = actionMgr.getCustomActionText(i)
22
- var desc = actionMgr.getCustomActionDesc(i)
23
- var icon = actionMgr.getCustomActionIcon(i)
34
+ const segments = normalized.split('/').filter(Boolean)
35
+ if (segments.length === 0) return ''
24
36
 
25
- return {
37
+ return segments.slice(Math.max(segments.length - segmentCount, 0)).join('/')
38
+ }
39
+
40
+ const getStablePathSuffixes = (action: CustomAction, filePath: string): string[] => {
41
+ const suffixes: string[] = []
42
+ const seen: Record<string, true> = {}
43
+
44
+ const addSuffixes = (value: string | null | undefined) => {
45
+ const normalized = normalizePath(value)
46
+ if (!normalized) return
47
+
48
+ const segments = normalized.split('/').filter(Boolean)
49
+ for (let segmentCount = segments.length; segmentCount >= 1; segmentCount -= 1) {
50
+ const suffix = segments.slice(segments.length - segmentCount).join('/')
51
+ if (!suffix || seen[suffix]) continue
52
+ seen[suffix] = true
53
+ suffixes.push(suffix)
54
+ }
55
+ }
56
+
57
+ addSuffixes(filePath)
58
+ addSuffixes(action.filePath)
59
+ return suffixes
60
+ }
61
+
62
+ const pathEndsWithSegments = (filePath: string | null | undefined, trailingSegments: string): boolean => {
63
+ const normalized = normalizePath(filePath)
64
+ if (!normalized || !trailingSegments) return false
65
+ return normalized === trailingSegments || normalized.endsWith(`/${trailingSegments}`)
66
+ }
67
+
68
+ export type CustomActionInstallState = {
69
+ action: CustomAction
70
+ customAction: CustomAction | null
71
+ installedMenu: boolean
72
+ installedToolbar: boolean
73
+ }
74
+
75
+ export type CustomActionTargets = {
76
+ menu: boolean
77
+ toolbar: boolean
78
+ }
79
+
80
+ type CustomActionCandidate = {
81
+ name: string
82
+ text: string
83
+ description: string
84
+ filePath: string
85
+ icon: string
86
+ shortcut: string
87
+ }
88
+
89
+ const cloneAction = (action: CustomAction): CustomAction => ({
90
+ ...action,
91
+ })
92
+
93
+ const resolveActionPaths = (action: CustomAction, scriptsPath: string = getScriptPath()): CustomAction => ({
94
+ ...action,
95
+ filePath: `${scriptsPath}/${action.filePath}`,
96
+ icon: action.icon ? `${scriptsPath}/${action.icon}` : ''
97
+ })
98
+
99
+ const getCustomActionCandidates = (action: CustomAction): CustomActionCandidate[] => {
100
+ const candidates: CustomActionCandidate[] = []
101
+ const actionsCount = actionMgr.getNumCustomActions()
102
+
103
+ for (let i = 0; i < actionsCount; i++) {
104
+ const actionFilePath = actionMgr.getCustomActionFile(i)
105
+ const name = actionMgr.getCustomActionName(i)
106
+ const text = actionMgr.getCustomActionText(i)
107
+ const desc = actionMgr.getCustomActionDesc(i)
108
+ const icon = actionMgr.getCustomActionIcon(i)
109
+
110
+ candidates.push({
26
111
  name: name.toString(),
27
112
  text: text.toString(),
28
113
  description: desc.toString(),
29
114
  filePath: actionFilePath.toString(),
30
115
  icon: icon.toString(),
31
- menuPath: action.menuPath,
32
- group: action.group,
33
- shortcut: action.shortcut,
34
- sort: action.sort,
35
- toolbar: action.toolbar
36
- } as CustomAction
116
+ shortcut: String(actionMgr.getCustomActionShortcut(i) ?? '').trim()
117
+ })
37
118
  }
38
119
 
39
- return null
120
+ return candidates
121
+ }
122
+
123
+ const toCustomAction = (candidate: CustomActionCandidate, action: CustomAction): CustomAction => ({
124
+ name: candidate.name,
125
+ text: candidate.text,
126
+ description: candidate.description,
127
+ filePath: candidate.filePath,
128
+ icon: candidate.icon,
129
+ shortcut: candidate.shortcut,
130
+ menuPath: action.menuPath,
131
+ group: action.group,
132
+ sort: action.sort,
133
+ toolbar: action.toolbar
134
+ })
135
+
136
+ const filterUniqueBy = (candidates: CustomActionCandidate[], predicate: (candidate: CustomActionCandidate) => boolean): CustomActionCandidate[] => {
137
+ const matches = candidates.filter(predicate)
138
+ return matches.length === 1 ? matches : []
139
+ }
140
+
141
+ const findByPathHeuristics = (action: CustomAction, filePath: string): CustomActionCandidate[] => {
142
+ const candidates = getCustomActionCandidates(action)
143
+ const normalizedTarget = normalizePath(filePath)
144
+
145
+ const exactMatches = filterUniqueBy(candidates, candidate => normalizePath(candidate.filePath) === normalizedTarget)
146
+ if (exactMatches.length > 0) return exactMatches
147
+
148
+ const stableSuffixes = getStablePathSuffixes(action, filePath)
149
+ for (const trailingSegments of stableSuffixes) {
150
+ if (!trailingSegments) continue
151
+
152
+ const suffixMatches = filterUniqueBy(candidates, candidate => pathEndsWithSegments(candidate.filePath, trailingSegments))
153
+ if (suffixMatches.length > 0) return suffixMatches
154
+ }
155
+
156
+ const expectedFileName = getFileName(filePath)
157
+ if (action.menuPath) {
158
+ const menuMatches = filterUniqueBy(candidates, candidate =>
159
+ getFileName(candidate.filePath) === expectedFileName &&
160
+ normalizePath(String(findMenuFor(candidate.name, actionMgr.getMenu())?.getPath() ?? '')) === normalizePath(action.menuPath)
161
+ )
162
+ if (menuMatches.length > 0) return menuMatches
163
+ }
164
+
165
+ if (action.toolbar) {
166
+ const toolbarMatches = filterUniqueBy(candidates, candidate =>
167
+ getFileName(candidate.filePath) === expectedFileName &&
168
+ isInstalledToToolbar(action.toolbar, candidate.name)
169
+ )
170
+ if (toolbarMatches.length > 0) return toolbarMatches
171
+ }
172
+
173
+ return []
174
+ }
175
+
176
+ export const findAllByFilePath = (action: CustomAction, filePath: string): CustomAction[] => {
177
+ return findByPathHeuristics(action, filePath).map(candidate => toCustomAction(candidate, action))
178
+ }
179
+
180
+ export const findByFilePath = (action: CustomAction, filePath: string): CustomAction | null => {
181
+ const matches = findAllByFilePath(action, filePath)
182
+ return matches.length > 0 ? matches[0] : null
40
183
  }
41
184
 
42
185
  export const findMenuFor = (actionName: string, topMenu?: DzActionMenu): DzActionMenu | null => {
43
186
  topMenu = topMenu ?? actionMgr.getMenu()
44
187
  if (!topMenu.hasItems()) return null
45
188
  for (let i = 0; i < topMenu.getNumItems(); i++) {
46
- let item = topMenu.getItem(i)
189
+ const item = topMenu.getItem(i)
47
190
  if (item.type == DzActionMenuItem.CustomAction && item.action == actionName)
48
191
  return item.getParentMenu()
49
192
  if (item.type == DzActionMenuItem.SubMenu) {
50
- var subMenu = findMenuFor(actionName, item.getSubMenu())
193
+ const subMenu = findMenuFor(actionName, item.getSubMenu())
51
194
  if (subMenu) return subMenu
52
195
  }
53
196
  }
@@ -55,11 +198,9 @@ export const findMenuFor = (actionName: string, topMenu?: DzActionMenu): DzActio
55
198
  }
56
199
 
57
200
  export const findInMenu = (menu: DzActionMenu, actionName: string): DzActionMenuItem | null => {
58
- var item: DzActionMenuItem
59
-
60
- var aItems = menu.getItemList()
61
- for (var i = 0; i < aItems.length; i += 1) {
62
- item = aItems[i]
201
+ const items = menu.getItemList()
202
+ for (let i = 0; i < items.length; i += 1) {
203
+ const item = items[i]
63
204
 
64
205
  if (item.type != DzActionMenuItem.CustomAction) {
65
206
  continue
@@ -73,91 +214,230 @@ export const findInMenu = (menu: DzActionMenu, actionName: string): DzActionMenu
73
214
  return null
74
215
  }
75
216
 
76
- export const createCustomAction = (action: CustomAction, update: boolean) => {
77
- let existingAction = findByFilePath(action, action.filePath)
217
+ const findToolbarItemIndex = (toolbar: DzToolBar, actionName: string): number => {
218
+ const toolbarItems = toolbar.getItemList()
219
+ for (let i = 0; i < toolbarItems.length; i++) {
220
+ const item = toolbarItems[i]
221
+ if (item.type !== DzToolBarItem.CustomAction) continue
222
+ if (item.action !== actionName) continue
223
+ return i
224
+ }
225
+ return -1
226
+ }
78
227
 
79
- if (existingAction && !update) return
228
+ const findToolbarItem = (toolbarName: string, actionName: string): DzToolBarItem | null => {
229
+ const toolbar = paneMgr.findToolBar(toolbarName)
230
+ if (!toolbar) return null
80
231
 
81
- if (existingAction) {
82
- removeCustomAction(existingAction)
232
+ const toolbarItems = toolbar.getItemList()
233
+ for (let i = 0; i < toolbarItems.length; i++) {
234
+ const item = toolbarItems[i]
235
+ if (item.type !== DzToolBarItem.CustomAction) continue
236
+ if (item.action !== actionName) continue
237
+ return item
83
238
  }
84
239
 
85
- let name = actionMgr.addCustomAction(String(action.text), String(action.description), String(action.filePath), true, action.shortcut ?? "", String(action.icon)).toString()
86
- action.name = name
240
+ return null
241
+ }
87
242
 
88
- if (action.shortcut) {
89
- actionMgr.setCustomActionShortcut(actionMgr.findCustomAction(name), action.shortcut)
90
- debug(`Created Custom Action: ${action.text} (${action.name}): ${action.filePath}`)
91
- }
243
+ const isInstalledToToolbar = (toolbarName: string | null | undefined, actionName: string | null | undefined): boolean => {
244
+ if (!toolbarName || !actionName) return false
245
+ return findToolbarItem(toolbarName, actionName) !== null
246
+ }
247
+
248
+ const removeFromMenu = (actionName: string | null | undefined) => {
249
+ if (!actionName) return
250
+ const menu = findMenuFor(actionName, actionMgr.getMenu())
251
+ if (!menu) return
92
252
 
93
- addToMenu(action)
94
- addToToolbar(action)
253
+ const menuAction = findInMenu(menu, actionName)
254
+ if (!menuAction) return
255
+
256
+ menu.removeItem(menuAction)
257
+ debug(`Action ${actionName} removed from menu`)
258
+ }
259
+
260
+ const removeFromToolbar = (toolbarName: string | null | undefined, actionName: string | null | undefined) => {
261
+ if (!toolbarName || !actionName) return
262
+ const toolbar = paneMgr.findToolBar(toolbarName)
263
+ if (!toolbar) return
264
+
265
+ const itemIndex = findToolbarItemIndex(toolbar, actionName)
266
+ if (itemIndex < 0) return
267
+
268
+ toolbar.removeItem(itemIndex)
269
+ debug(`Action ${actionName} removed from toolbar ${toolbarName} (index ${itemIndex})`)
270
+ }
271
+
272
+ const removeUnderlyingCustomAction = (actionName: string | null | undefined) => {
273
+ if (!actionName) return
274
+
275
+ const index = actionMgr.findCustomAction(actionName)
276
+ if (index < 0) return
277
+
278
+ actionMgr.removeCustomAction(index)
279
+ debug(`Action ${actionName} removed`)
280
+ }
281
+
282
+ export const clearToolbar = (toolbarName: string | null | undefined) => {
283
+ if (!toolbarName) return
284
+ const toolbar = paneMgr.findToolBar(toolbarName)
285
+ if (!toolbar) return
286
+ toolbar.clear()
287
+ debug(`Toolbar ${toolbarName} cleared`)
288
+ }
289
+
290
+ export const cleanupEmptyToolbar = (toolbarName: string | null | undefined) => {
291
+ if (!toolbarName) return
292
+ const toolbar = paneMgr.findToolBar(toolbarName)
293
+ if (!toolbar) return
294
+
295
+ if (toolbar.hasItems() || toolbar.getItemList().length > 0) return
296
+
297
+ toolbar.setClosed(true)
298
+ debug(`Toolbar ${toolbarName} closed (setClosed=true)`)
299
+ toolbar.clear()
300
+ debug(`Toolbar ${toolbarName} cleared`)
301
+ paneMgr.removeToolBar(toolbar)
302
+ debug(`Toolbar ${toolbarName} removeToolBar called`)
95
303
  }
96
304
 
97
305
  export const addToMenu = (action: CustomAction) => {
98
- if (!action.menuPath) return
99
- action.menuPath = action.menuPath + '/'
100
- var menu = getMenu(action.menuPath, true)
101
- var menuAction = findInMenu(menu, action.name)
306
+ if (!action.menuPath || !action.name) return
307
+ const menuPath = `${action.menuPath}/`
308
+ const menu = getMenu(menuPath, true)
309
+ const menuAction = findInMenu(menu, action.name)
102
310
  if (menuAction) return
103
311
  menu.insertCustomAction(action.name, action.sort ?? -1)
104
312
  debug(`Action "${action.text}" added to menu "${menu.getPath()}"`)
105
313
  }
106
314
 
107
315
  export const addToToolbar = (action: CustomAction) => {
108
- if (!action.toolbar) return;
316
+ if (!action.toolbar || !action.name) return
109
317
 
110
- var toolbar = paneMgr.findToolBar(action.toolbar);
318
+ let toolbar = paneMgr.findToolBar(action.toolbar)
111
319
 
112
320
  if (!toolbar) {
113
- toolbar = paneMgr.createToolBar(action.toolbar);
114
- toolbar.dock(DzToolBar.ToolBarTop);
321
+ toolbar = paneMgr.createToolBar(action.toolbar)
322
+ toolbar.dock(DzToolBar.ToolBarTop)
323
+ } else {
324
+ toolbar.setClosed(false)
115
325
  }
116
326
 
117
- if (array.find(toolbar.getItemList(), i => i.action == action.name)) return
327
+ if (findToolbarItem(action.toolbar, action.name)) return
118
328
 
119
- debug(`Adding menu to toolbar ${toolbar.name}`)
120
- toolbar.insertCustomAction(action.name, action.sort)
329
+ toolbar.insertCustomAction(action.name, action.sort ?? -1)
330
+ debug(`Action "${action.text}" added to toolbar "${action.toolbar}"`)
121
331
  }
122
332
 
123
- const removeCustomAction = (action: CustomAction) => {
124
- if (!action.name) return
125
- var menu = findMenuFor(action.name, actionMgr.getMenu())
126
- if (menu) {
127
- var menuAction = findInMenu(menu, action.name)
128
- if (menuAction) {
129
- menu.removeItem(menuAction)
130
- debug(`Action ${action.text} Removed from menu`)
131
- }
333
+ const createOrUpdateCustomAction = (sourceAction: CustomAction, resolvedAction: CustomAction): CustomAction => {
334
+ const existingActions = findAllByFilePath(sourceAction, resolvedAction.filePath)
335
+ existingActions.forEach((existingAction) => {
336
+ removeFromMenu(existingAction.name)
337
+ removeFromToolbar(existingAction.toolbar, existingAction.name)
338
+ removeUnderlyingCustomAction(existingAction.name)
339
+ })
340
+
341
+ const name = actionMgr
342
+ .addCustomAction(
343
+ String(resolvedAction.text),
344
+ String(resolvedAction.description),
345
+ String(resolvedAction.filePath),
346
+ true,
347
+ resolvedAction.shortcut ?? "",
348
+ String(resolvedAction.icon ?? '')
349
+ )
350
+ .toString()
351
+
352
+ const created = {
353
+ ...resolvedAction,
354
+ name
355
+ } as CustomAction
356
+
357
+ if (resolvedAction.shortcut) {
358
+ actionMgr.setCustomActionShortcut(actionMgr.findCustomAction(name), resolvedAction.shortcut)
132
359
  }
133
360
 
134
- actionMgr.removeCustomAction(actionMgr.findCustomAction(action.name))
135
- debug(`Action ${action.text} Removed`)
136
- debug(`Toolbar: ${action.toolbar}`)
361
+ debug(`Created Custom Action: ${created.text} (${created.name}): ${created.filePath}`)
362
+ return created
363
+ }
137
364
 
138
- if (!action.toolbar) return
139
- debug(`Removing Toolbar ${action.toolbar}`)
140
- var toolbar = paneMgr.findToolBar(action.toolbar)
141
- if (!toolbar) return
142
- toolbar.clear()
143
- // paneMgr.removeToolBar(action.toolbar)
144
- // debug(`Toolbar Removed`)
365
+ export const getInstalledCustomActionState = (action: CustomAction, scriptsPath: string = getScriptPath()): CustomActionInstallState => {
366
+ const resolvedAction = resolveActionPaths(action, scriptsPath)
367
+ const customAction = findByFilePath(action, resolvedAction.filePath)
368
+ const installedMenu = customAction?.name ? findMenuFor(customAction.name, actionMgr.getMenu()) !== null : false
369
+ const installedToolbar = customAction?.name ? isInstalledToToolbar(action.toolbar, customAction.name) : false
370
+
371
+ return {
372
+ action: resolvedAction,
373
+ customAction,
374
+ installedMenu,
375
+ installedToolbar
376
+ }
377
+ }
378
+
379
+ export const applyCustomActionTargets = (action: CustomAction, targets: CustomActionTargets, scriptsPath: string = getScriptPath()) => {
380
+ const resolvedAction = resolveActionPaths(action, scriptsPath)
381
+ const shouldInstall = targets.menu || targets.toolbar
382
+
383
+ if (!shouldInstall) {
384
+ removeCustomActionTargets(action, { menu: true, toolbar: true }, scriptsPath)
385
+ return
386
+ }
387
+
388
+ const customAction = createOrUpdateCustomAction(action, resolvedAction)
389
+
390
+ if (targets.menu && action.menuPath) {
391
+ addToMenu(customAction)
392
+ }
393
+
394
+ if (targets.toolbar && action.toolbar) {
395
+ addToToolbar(customAction)
396
+ }
397
+ }
398
+
399
+ export const removeCustomActionTargets = (action: CustomAction, targets: CustomActionTargets, scriptsPath: string = getScriptPath()) => {
400
+ const resolvedAction = resolveActionPaths(action, scriptsPath)
401
+ const matches = findAllByFilePath(action, resolvedAction.filePath)
402
+
403
+ if (matches.length === 0) return
404
+
405
+ matches.forEach((match) => {
406
+ const installedMenu = match.name ? findMenuFor(match.name, actionMgr.getMenu()) !== null : false
407
+ const installedToolbar = match.name ? isInstalledToToolbar(action.toolbar, match.name) : false
408
+
409
+ if (!match.name) return
410
+
411
+ if (targets.menu && installedMenu) {
412
+ removeFromMenu(match.name)
413
+ }
414
+
415
+ if (targets.toolbar && installedToolbar) {
416
+ removeFromToolbar(action.toolbar, match.name)
417
+ }
418
+
419
+ const menuStillInstalled = !targets.menu && installedMenu
420
+ const toolbarStillInstalled = !targets.toolbar && installedToolbar
421
+
422
+ if (!menuStillInstalled && !toolbarStillInstalled) {
423
+ removeUnderlyingCustomAction(match.name)
424
+ }
425
+ })
145
426
  }
146
427
 
147
428
  export const installCustomActions = (actions: CustomAction[]) => {
148
- uninstallCustomActions(actions)
149
- let scriptsPath = getScriptPath()
429
+ const scriptsPath = getScriptPath()
150
430
  debug(`Script path: ${scriptsPath}`)
151
- let menuPaths = array.group(actions, (a => a.menuPath ?? ""))
431
+ const menuPaths = array.group(actions, (a => a.menuPath ?? ""))
152
432
  progress('Installing', keys(menuPaths), (menuPath) => {
153
- let actions = menuPaths[menuPath] as CustomAction[]
154
- let groups = array.group(actions, (action) => action.group ?? "")
433
+ const groupedActions = menuPaths[menuPath] as CustomAction[]
434
+ const groups = array.group(groupedActions, (action) => action.group ?? "")
155
435
  keys(groups).forEach(group => {
156
436
  groups[group].forEach(action => {
157
- action.filePath = `${scriptsPath}/${action.filePath}`
158
- action.icon = action.icon ? `${scriptsPath}/${action.icon}` : ''
159
- createCustomAction(action, true)
160
- // findMenuFor(action.name)
437
+ applyCustomActionTargets(action, {
438
+ menu: Boolean(action.menuPath),
439
+ toolbar: Boolean(action.toolbar)
440
+ }, scriptsPath)
161
441
  })
162
442
  })
163
443
  })
@@ -165,12 +445,23 @@ export const installCustomActions = (actions: CustomAction[]) => {
165
445
 
166
446
  export const uninstallCustomActions = (actions: CustomAction[]) => {
167
447
  debug(`Uninstalling Actions`)
448
+ const toolbarNames = new Set<string>()
449
+
168
450
  actions.forEach(action => {
169
451
  if (!action) return
170
- let customAction = findByFilePath(action, `${getScriptPath()}/${action.filePath}`)
171
- if (!customAction) return
172
- debug(`Uninstalling action "${customAction.text} (${customAction.name})"`)
173
- removeCustomAction(customAction)
452
+ if (action.toolbar) toolbarNames.add(action.toolbar)
453
+ removeCustomActionTargets(action, { menu: true, toolbar: true })
174
454
  })
175
- }
176
455
 
456
+ toolbarNames.forEach(toolbarName => {
457
+ const toolbar = paneMgr.findToolBar(toolbarName)
458
+ if (!toolbar) {
459
+ debug(`Toolbar ${toolbarName} not found for post-uninstall cleanup`)
460
+ return
461
+ }
462
+ toolbar.clear()
463
+ toolbar.setClosed(true)
464
+ paneMgr.removeToolBar(toolbar)
465
+ debug(`Toolbar ${toolbarName} cleared, closed, and removed after uninstall`)
466
+ })
467
+ }