6
6
import { RunOnceScheduler } from 'vs/base/common/async' ;
7
7
import { Emitter , Event } from 'vs/base/common/event' ;
8
8
import { DisposableStore } from 'vs/base/common/lifecycle' ;
9
- import { IMenu , IMenuActionOptions , IMenuCreateOptions , IMenuItem , IMenuService , isIMenuItem , ISubmenuItem , MenuId , MenuItemAction , MenuRegistry , SubmenuItemAction } from 'vs/platform/actions/common/actions' ;
10
- import { ILocalizedString } from 'vs/platform/action/common/action' ;
9
+ import { IMenu , IMenuActionOptions , IMenuCreateOptions , IMenuItem , IMenuService , isIMenuItem , ISubmenuItem , MenuId , MenuItemAction , MenuItemActionManageActions , MenuRegistry , SubmenuItemAction } from 'vs/platform/actions/common/actions' ;
10
+ import { ICommandAction , ILocalizedString } from 'vs/platform/action/common/action' ;
11
11
import { ICommandService } from 'vs/platform/commands/common/commands' ;
12
12
import { ContextKeyExpression , IContextKeyService } from 'vs/platform/contextkey/common/contextkey' ;
13
+ import { IAction , SubmenuAction } from 'vs/base/common/actions' ;
14
+ import { IStorageService , StorageScope , StorageTarget } from 'vs/platform/storage/common/storage' ;
15
+ import { removeFastWithoutKeepingOrder } from 'vs/base/common/arrays' ;
16
+ import { localize } from 'vs/nls' ;
13
17
14
18
export class MenuService implements IMenuService {
15
19
16
20
declare readonly _serviceBrand : undefined ;
17
21
22
+ private readonly _hiddenStates : PersistedMenuHideState ;
23
+
18
24
constructor (
19
- @ICommandService private readonly _commandService : ICommandService
25
+ @ICommandService private readonly _commandService : ICommandService ,
26
+ @IStorageService storageService : IStorageService ,
20
27
) {
21
- //
28
+ this . _hiddenStates = new PersistedMenuHideState ( storageService ) ;
22
29
}
23
30
24
31
/**
25
32
* Create a new menu for the given menu identifier. A menu sends events when it's entries
26
- * have changed (placement, enablement, checked-state). By default it does send events for
27
- * sub menu entries. That is more expensive and must be explicitly enabled with the
33
+ * have changed (placement, enablement, checked-state). By default it does not send events for
34
+ * submenu entries. That is more expensive and must be explicitly enabled with the
28
35
* `emitEventsForSubmenuChanges` flag.
29
36
*/
30
37
createMenu ( id : MenuId , contextKeyService : IContextKeyService , options ?: IMenuCreateOptions ) : IMenu {
31
- return new Menu ( id , { emitEventsForSubmenuChanges : false , eventDebounceDelay : 50 , ...options } , this . _commandService , contextKeyService , this ) ;
38
+ return new Menu ( id , this . _hiddenStates , { emitEventsForSubmenuChanges : false , eventDebounceDelay : 50 , ...options } , this . _commandService , contextKeyService , this ) ;
32
39
}
33
40
}
34
41
42
+ class PersistedMenuHideState {
43
+
44
+ private static readonly _key = 'menu.hiddenCommands' ;
45
+
46
+ readonly onDidChange : Event < any > ;
47
+ private readonly _disposables = new DisposableStore ( ) ;
48
+ private readonly _data : Record < string , string [ ] | undefined > ;
49
+
50
+ constructor ( @IStorageService private readonly _storageService : IStorageService ) {
51
+ try {
52
+ const raw = _storageService . get ( PersistedMenuHideState . _key , StorageScope . PROFILE , '{}' ) ;
53
+ this . _data = JSON . parse ( raw ) ;
54
+ } catch ( err ) {
55
+ this . _data = Object . create ( null ) ;
56
+ }
57
+
58
+ this . onDidChange = Event . filter ( _storageService . onDidChangeValue , e => e . key === PersistedMenuHideState . _key , this . _disposables ) ;
59
+ }
60
+
61
+ dispose ( ) {
62
+ this . _disposables . dispose ( ) ;
63
+ }
64
+
65
+ isHidden ( menu : MenuId , commandId : string ) : boolean {
66
+ return this . _data [ menu . id ] ?. includes ( commandId ) ?? false ;
67
+ }
68
+
69
+ updateHidden ( menu : MenuId , commandId : string , hidden : boolean ) : void {
70
+ const entries = this . _data [ menu . id ] ;
71
+ if ( ! hidden ) {
72
+ // remove and cleanup
73
+ if ( entries ) {
74
+ const idx = entries . indexOf ( commandId ) ;
75
+ if ( idx >= 0 ) {
76
+ removeFastWithoutKeepingOrder ( entries , idx ) ;
77
+ }
78
+ if ( entries . length === 0 ) {
79
+ delete this . _data [ menu . id ] ;
80
+ }
81
+ }
82
+ } else {
83
+ // add unless already added
84
+ if ( ! entries ) {
85
+ this . _data [ menu . id ] = [ commandId ] ;
86
+ } else {
87
+ const idx = entries . indexOf ( commandId ) ;
88
+ if ( idx < 0 ) {
89
+ entries . push ( commandId ) ;
90
+ }
91
+ }
92
+ }
93
+ this . _persist ( ) ;
94
+ }
95
+
96
+ private _persist ( ) : void {
97
+ const raw = JSON . stringify ( this . _data ) ;
98
+ this . _storageService . store ( PersistedMenuHideState . _key , raw , StorageScope . PROFILE , StorageTarget . USER ) ;
99
+ }
100
+ }
35
101
36
102
type MenuItemGroup = [ string , Array < IMenuItem | ISubmenuItem > ] ;
37
103
@@ -47,6 +113,7 @@ class Menu implements IMenu {
47
113
48
114
constructor (
49
115
private readonly _id : MenuId ,
116
+ private readonly _hiddenStates : PersistedMenuHideState ,
50
117
private readonly _options : Required < IMenuCreateOptions > ,
51
118
@ICommandService private readonly _commandService : ICommandService ,
52
119
@IContextKeyService private readonly _contextKeyService : IContextKeyService ,
@@ -68,24 +135,27 @@ class Menu implements IMenu {
68
135
}
69
136
} ) ) ;
70
137
71
- // When context keys change we need to check if the menu also has changed. However,
72
- // we only do that when someone listens on this menu because (1) context key events are
138
+ // When context keys or storage state changes we need to check if the menu also has changed. However,
139
+ // we only do that when someone listens on this menu because (1) these events are
73
140
// firing often and (2) menu are often leaked
74
- const contextKeyListener = this . _disposables . add ( new DisposableStore ( ) ) ;
75
- const startContextKeyListener = ( ) => {
141
+ const lazyListener = this . _disposables . add ( new DisposableStore ( ) ) ;
142
+ const startLazyListener = ( ) => {
76
143
const fireChangeSoon = new RunOnceScheduler ( ( ) => this . _onDidChange . fire ( this ) , _options . eventDebounceDelay ) ;
77
- contextKeyListener . add ( fireChangeSoon ) ;
78
- contextKeyListener . add ( _contextKeyService . onDidChangeContext ( e => {
144
+ lazyListener . add ( fireChangeSoon ) ;
145
+ lazyListener . add ( _contextKeyService . onDidChangeContext ( e => {
79
146
if ( e . affectsSome ( this . _contextKeys ) ) {
80
147
fireChangeSoon . schedule ( ) ;
81
148
}
82
149
} ) ) ;
150
+ lazyListener . add ( _hiddenStates . onDidChange ( ( ) => {
151
+ fireChangeSoon . schedule ( ) ;
152
+ } ) ) ;
83
153
} ;
84
154
85
155
this . _onDidChange = new Emitter ( {
86
156
// start/stop context key listener
87
- onFirstListenerAdd : startContextKeyListener ,
88
- onLastListenerRemove : contextKeyListener . clear . bind ( contextKeyListener )
157
+ onFirstListenerAdd : startLazyListener ,
158
+ onLastListenerRemove : lazyListener . clear . bind ( lazyListener )
89
159
} ) ;
90
160
this . onDidChange = this . _onDidChange . event ;
91
161
@@ -145,20 +215,47 @@ class Menu implements IMenu {
145
215
146
216
getActions ( options ?: IMenuActionOptions ) : [ string , Array < MenuItemAction | SubmenuItemAction > ] [ ] {
147
217
const result : [ string , Array < MenuItemAction | SubmenuItemAction > ] [ ] = [ ] ;
218
+ const allToggleActions : IAction [ ] [ ] = [ ] ;
219
+
148
220
for ( const group of this . _menuGroups ) {
149
221
const [ id , items ] = group ;
222
+
223
+ const toggleActions : IAction [ ] = [ ] ;
224
+
150
225
const activeActions : Array < MenuItemAction | SubmenuItemAction > = [ ] ;
151
226
for ( const item of items ) {
152
227
if ( this . _contextKeyService . contextMatchesRules ( item . when ) ) {
153
228
let action : MenuItemAction | SubmenuItemAction | undefined ;
154
229
if ( isIMenuItem ( item ) ) {
155
- action = new MenuItemAction ( item . command , item . alt , options , this . _contextKeyService , this . _commandService ) ;
230
+ if ( ! this . _hiddenStates . isHidden ( this . _id , item . command . id ) ) {
231
+ action = new MenuItemAction (
232
+ item . command , item . alt , options ,
233
+ new MenuItemActionManageActions ( new HideMenuItemAction ( this . _id , item . command , this . _hiddenStates ) , allToggleActions ) ,
234
+ this . _contextKeyService , this . _commandService
235
+ ) ;
236
+ }
237
+ // add toggle commmand
238
+ toggleActions . push ( new ToggleMenuItemAction ( this . _id , item . command , this . _hiddenStates ) ) ;
156
239
} else {
157
240
action = new SubmenuItemAction ( item , this . _menuService , this . _contextKeyService , options ) ;
158
241
if ( action . actions . length === 0 ) {
159
242
action . dispose ( ) ;
160
243
action = undefined ;
161
244
}
245
+ // add toggle submenu
246
+ if ( action ) {
247
+ // todo@jrieken this isn't good and O(n2) because this recurses for each submenu...
248
+ const makeToggleCommand = ( id : MenuId , action : IAction ) : IAction => {
249
+ if ( action instanceof SubmenuItemAction ) {
250
+ return new SubmenuAction ( action . id , action . label , action . actions . map ( a => makeToggleCommand ( action . item . submenu , a ) ) ) ;
251
+ } else if ( action instanceof MenuItemAction ) {
252
+ return new ToggleMenuItemAction ( id , action . item , this . _hiddenStates ) ;
253
+ } else {
254
+ return action ;
255
+ }
256
+ } ;
257
+ toggleActions . push ( makeToggleCommand ( this . _id , action ) ) ;
258
+ }
162
259
}
163
260
164
261
if ( action ) {
@@ -169,6 +266,9 @@ class Menu implements IMenu {
169
266
if ( activeActions . length > 0 ) {
170
267
result . push ( [ id , activeActions ] ) ;
171
268
}
269
+ if ( toggleActions . length > 0 ) {
270
+ allToggleActions . push ( toggleActions ) ;
271
+ }
172
272
}
173
273
return result ;
174
274
}
@@ -231,3 +331,55 @@ class Menu implements IMenu {
231
331
return aStr . localeCompare ( bStr ) ;
232
332
}
233
333
}
334
+
335
+ class ToggleMenuItemAction implements IAction {
336
+
337
+ readonly id : string ;
338
+ readonly label : string ;
339
+ readonly enabled : boolean = true ;
340
+ readonly tooltip : string = '' ;
341
+
342
+ readonly checked : boolean ;
343
+ readonly class : undefined ;
344
+
345
+ run : ( ) => void ;
346
+
347
+ constructor ( id : MenuId , command : ICommandAction , hiddenStates : PersistedMenuHideState ) {
348
+ this . id = `toggle/${ id . id } /${ command . id } ` ;
349
+ this . label = typeof command . title === 'string' ? command . title : command . title . value ;
350
+
351
+ let isHidden = hiddenStates . isHidden ( id , command . id ) ;
352
+ this . checked = ! isHidden ;
353
+ this . run = ( ) => {
354
+ isHidden = ! isHidden ;
355
+ hiddenStates . updateHidden ( id , command . id , isHidden ) ;
356
+ } ;
357
+ }
358
+
359
+ dispose ( ) : void {
360
+ // NOTHING
361
+ }
362
+ }
363
+
364
+ class HideMenuItemAction implements IAction {
365
+
366
+ readonly id : string ;
367
+ readonly label : string ;
368
+ readonly enabled : boolean = true ;
369
+ readonly tooltip : string = '' ;
370
+
371
+ readonly checked : undefined ;
372
+ readonly class : undefined ;
373
+
374
+ run : ( ) => void ;
375
+
376
+ constructor ( id : MenuId , command : ICommandAction , hiddenStates : PersistedMenuHideState ) {
377
+ this . id = `hide/${ id . id } /${ command . id } ` ;
378
+ this . label = localize ( 'hide.label' , 'Hide \'{0}\'' , typeof command . title === 'string' ? command . title : command . title . value ) ;
379
+ this . run = ( ) => { hiddenStates . updateHidden ( id , command . id , true ) ; } ;
380
+ }
381
+
382
+ dispose ( ) : void {
383
+ // NOTHING
384
+ }
385
+ }
0 commit comments