@opensumi/ide-main-layout 2.21.13 → 2.22.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/lib/browser/accordion/accordion.service.js +1 -1
- package/lib/browser/accordion/accordion.service.js.map +1 -1
- package/lib/browser/accordion/styles.module.less +4 -2
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/layout.service.d.ts.map +1 -1
- package/lib/browser/layout.service.js +6 -6
- package/lib/browser/layout.service.js.map +1 -1
- package/lib/browser/main-layout.contribution.js +1 -1
- package/lib/browser/main-layout.contribution.js.map +1 -1
- package/lib/browser/quick-open-view.js +3 -2
- package/lib/browser/quick-open-view.js.map +1 -1
- package/lib/browser/tabbar/tabbar.service.js.map +1 -1
- package/lib/browser/tabbar-handler.js.map +1 -1
- package/lib/browser/views-registry.js +7 -7
- package/lib/browser/views-registry.js.map +1 -1
- package/package.json +10 -9
- package/src/browser/accordion/accordion.service.ts +635 -0
- package/src/browser/accordion/accordion.view.tsx +102 -0
- package/src/browser/accordion/section.view.tsx +181 -0
- package/src/browser/accordion/styles.module.less +214 -0
- package/src/browser/accordion/titlebar.view.tsx +17 -0
- package/src/browser/default-config.ts +35 -0
- package/src/browser/index.ts +40 -0
- package/src/browser/input/index.tsx +32 -0
- package/src/browser/layout.service.ts +515 -0
- package/src/browser/main-layout.contribution.ts +447 -0
- package/src/browser/quick-open-view.ts +138 -0
- package/src/browser/tabbar/bar.view.tsx +289 -0
- package/src/browser/tabbar/panel.view.tsx +190 -0
- package/src/browser/tabbar/renderer.view.tsx +133 -0
- package/src/browser/tabbar/styles.module.less +408 -0
- package/src/browser/tabbar/tabbar.service.ts +845 -0
- package/src/browser/tabbar-handler.ts +200 -0
- package/src/browser/views-registry.ts +157 -0
- package/src/browser/welcome.view.tsx +152 -0
- package/src/common/index.ts +2 -0
- package/src/common/main-layout.definition.ts +122 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
import debounce from 'lodash/debounce';
|
|
2
|
+
import { observable, action, observe, computed } from 'mobx';
|
|
3
|
+
|
|
4
|
+
import { Injectable, Autowired } from '@opensumi/di';
|
|
5
|
+
import {
|
|
6
|
+
toDisposable,
|
|
7
|
+
WithEventBus,
|
|
8
|
+
ComponentRegistryInfo,
|
|
9
|
+
Emitter,
|
|
10
|
+
Event,
|
|
11
|
+
OnEvent,
|
|
12
|
+
ResizeEvent,
|
|
13
|
+
SlotLocation,
|
|
14
|
+
CommandRegistry,
|
|
15
|
+
localize,
|
|
16
|
+
KeybindingRegistry,
|
|
17
|
+
ViewContextKeyRegistry,
|
|
18
|
+
IContextKeyService,
|
|
19
|
+
getTabbarCtxKey,
|
|
20
|
+
IContextKey,
|
|
21
|
+
DisposableCollection,
|
|
22
|
+
IScopedContextKeyService,
|
|
23
|
+
Deferred,
|
|
24
|
+
formatLocalize,
|
|
25
|
+
} from '@opensumi/ide-core-browser';
|
|
26
|
+
import { SCM_CONTAINER_ID } from '@opensumi/ide-core-browser/lib/common/container-id';
|
|
27
|
+
import { ResizeHandle } from '@opensumi/ide-core-browser/lib/components';
|
|
28
|
+
import { LayoutState, LAYOUT_STATE } from '@opensumi/ide-core-browser/lib/layout/layout-state';
|
|
29
|
+
import {
|
|
30
|
+
AbstractContextMenuService,
|
|
31
|
+
AbstractMenuService,
|
|
32
|
+
IContextMenu,
|
|
33
|
+
IMenuRegistry,
|
|
34
|
+
ICtxMenuRenderer,
|
|
35
|
+
generateCtxMenu,
|
|
36
|
+
IMenu,
|
|
37
|
+
MenuId,
|
|
38
|
+
getTabbarCommonMenuId,
|
|
39
|
+
} from '@opensumi/ide-core-browser/lib/menu/next';
|
|
40
|
+
import { IProgressService } from '@opensumi/ide-core-browser/lib/progress';
|
|
41
|
+
|
|
42
|
+
import { TabBarRegistrationEvent, IMainLayoutService, SUPPORT_ACCORDION_LOCATION } from '../../common';
|
|
43
|
+
import { TOGGLE_BOTTOM_PANEL_COMMAND, EXPAND_BOTTOM_PANEL, RETRACT_BOTTOM_PANEL } from '../main-layout.contribution';
|
|
44
|
+
|
|
45
|
+
export const TabbarServiceFactory = Symbol('TabbarServiceFactory');
|
|
46
|
+
export interface TabState {
|
|
47
|
+
hidden: boolean;
|
|
48
|
+
// 排序位置,数字越小优先级越高
|
|
49
|
+
priority: number;
|
|
50
|
+
}
|
|
51
|
+
const CONTAINER_NAME_MAP = {
|
|
52
|
+
left: 'view',
|
|
53
|
+
right: 'extendView',
|
|
54
|
+
bottom: 'panel',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
@Injectable({ multiple: true })
|
|
58
|
+
export class TabbarService extends WithEventBus {
|
|
59
|
+
@observable currentContainerId: string;
|
|
60
|
+
|
|
61
|
+
previousContainerId = '';
|
|
62
|
+
|
|
63
|
+
// 由于 observable.map (即使是deep:false) 会把值转换成observableValue,不希望这样
|
|
64
|
+
containersMap: Map<string, ComponentRegistryInfo> = new Map();
|
|
65
|
+
@observable state: Map<string, TabState> = new Map();
|
|
66
|
+
|
|
67
|
+
private storedState: { [containerId: string]: TabState } = {};
|
|
68
|
+
|
|
69
|
+
public prevSize?: number;
|
|
70
|
+
public commonTitleMenu: IContextMenu;
|
|
71
|
+
|
|
72
|
+
public viewReady = new Deferred<void>();
|
|
73
|
+
|
|
74
|
+
resizeHandle?: {
|
|
75
|
+
setSize: (targetSize?: number) => void;
|
|
76
|
+
setRelativeSize: (prev: number, next: number) => void;
|
|
77
|
+
getSize: () => number;
|
|
78
|
+
getRelativeSize: () => number[];
|
|
79
|
+
lockSize: (lock: boolean | undefined) => void;
|
|
80
|
+
setMaxSize: (lock: boolean | undefined) => void;
|
|
81
|
+
hidePanel: (show?: boolean) => void;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
@Autowired(AbstractMenuService)
|
|
85
|
+
protected menuService: AbstractMenuService;
|
|
86
|
+
|
|
87
|
+
@Autowired(AbstractContextMenuService)
|
|
88
|
+
private readonly ctxMenuService: AbstractContextMenuService;
|
|
89
|
+
|
|
90
|
+
@Autowired(IMenuRegistry)
|
|
91
|
+
protected menuRegistry: IMenuRegistry;
|
|
92
|
+
|
|
93
|
+
@Autowired(CommandRegistry)
|
|
94
|
+
private commandRegistry: CommandRegistry;
|
|
95
|
+
|
|
96
|
+
@Autowired(ICtxMenuRenderer)
|
|
97
|
+
private readonly contextMenuRenderer: ICtxMenuRenderer;
|
|
98
|
+
|
|
99
|
+
@Autowired(KeybindingRegistry)
|
|
100
|
+
private keybindingRegistry: KeybindingRegistry;
|
|
101
|
+
|
|
102
|
+
@Autowired()
|
|
103
|
+
private viewContextKeyRegistry: ViewContextKeyRegistry;
|
|
104
|
+
|
|
105
|
+
@Autowired(IContextKeyService)
|
|
106
|
+
private contextKeyService: IContextKeyService;
|
|
107
|
+
|
|
108
|
+
@Autowired(IMainLayoutService)
|
|
109
|
+
private layoutService: IMainLayoutService;
|
|
110
|
+
|
|
111
|
+
@Autowired()
|
|
112
|
+
private layoutState: LayoutState;
|
|
113
|
+
|
|
114
|
+
@Autowired(IProgressService)
|
|
115
|
+
private progressService: IProgressService;
|
|
116
|
+
|
|
117
|
+
// 提供给Mobx强刷
|
|
118
|
+
@observable forceUpdate = 0;
|
|
119
|
+
|
|
120
|
+
private accordionRestored: Set<string> = new Set();
|
|
121
|
+
|
|
122
|
+
private readonly onCurrentChangeEmitter = new Emitter<{ previousId: string; currentId: string }>();
|
|
123
|
+
readonly onCurrentChange: Event<{ previousId: string; currentId: string }> = this.onCurrentChangeEmitter.event;
|
|
124
|
+
|
|
125
|
+
private readonly onSizeChangeEmitter = new Emitter<{ size: number }>();
|
|
126
|
+
readonly onSizeChange: Event<{ size: number }> = this.onSizeChangeEmitter.event;
|
|
127
|
+
|
|
128
|
+
public barSize: number;
|
|
129
|
+
public panelSize: number;
|
|
130
|
+
private menuId = `tabbar/${this.location}`;
|
|
131
|
+
private moreMenuId = `tabbar/${this.location}/more`;
|
|
132
|
+
private isLatter = this.location === SlotLocation.right || this.location === SlotLocation.bottom;
|
|
133
|
+
private activatedKey: IContextKey<string>;
|
|
134
|
+
private sortedContainers: Array<ComponentRegistryInfo> = [];
|
|
135
|
+
private disposableMap: Map<string, DisposableCollection> = new Map();
|
|
136
|
+
private tabInMoreKeyMap: Map<string, IContextKey<boolean>> = new Map();
|
|
137
|
+
|
|
138
|
+
private scopedCtxKeyService: IScopedContextKeyService;
|
|
139
|
+
|
|
140
|
+
constructor(public location: string) {
|
|
141
|
+
super();
|
|
142
|
+
this.scopedCtxKeyService = this.contextKeyService.createScoped();
|
|
143
|
+
this.scopedCtxKeyService.createKey('triggerWithTab', true);
|
|
144
|
+
this.menuRegistry.registerMenuItem(this.menuId, {
|
|
145
|
+
command: {
|
|
146
|
+
id: this.registerGlobalToggleCommand(),
|
|
147
|
+
label: localize('layout.tabbar.toggle'),
|
|
148
|
+
},
|
|
149
|
+
group: '0_global',
|
|
150
|
+
when: 'triggerWithTab == true',
|
|
151
|
+
});
|
|
152
|
+
this.activatedKey = this.contextKeyService.createKey(getTabbarCtxKey(this.location), '');
|
|
153
|
+
if (this.location === 'bottom') {
|
|
154
|
+
this.registerPanelCommands();
|
|
155
|
+
this.registerPanelMenus();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
registerPanelCommands(): void {
|
|
160
|
+
this.commandRegistry.registerCommand(EXPAND_BOTTOM_PANEL, {
|
|
161
|
+
execute: () => {
|
|
162
|
+
this.layoutService.expandBottom(true);
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
this.commandRegistry.registerCommand(RETRACT_BOTTOM_PANEL, {
|
|
166
|
+
execute: () => {
|
|
167
|
+
this.layoutService.expandBottom(false);
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
this.commandRegistry.registerCommand(TOGGLE_BOTTOM_PANEL_COMMAND, {
|
|
171
|
+
execute: (show?: boolean, size?: number) => {
|
|
172
|
+
this.layoutService.toggleSlot(SlotLocation.bottom, show, size);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public getContainerState(containerId: string) {
|
|
178
|
+
const viewState = this.state.get(containerId);
|
|
179
|
+
return viewState!;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private updatePanel = debounce((show) => {
|
|
183
|
+
if (this.resizeHandle) {
|
|
184
|
+
this.resizeHandle.hidePanel(show);
|
|
185
|
+
}
|
|
186
|
+
}, 60);
|
|
187
|
+
|
|
188
|
+
public updatePanelVisibility(show?: boolean) {
|
|
189
|
+
if (show === undefined) {
|
|
190
|
+
show = this.containersMap.size > 0;
|
|
191
|
+
}
|
|
192
|
+
this.updatePanel(show);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
public updateTabInMoreKey(containerId: string, value: boolean) {
|
|
196
|
+
const ctxKey = this.tabInMoreKeyMap.get(containerId);
|
|
197
|
+
if (ctxKey) {
|
|
198
|
+
ctxKey.set(value);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@computed({ equals: visibleContainerEquals })
|
|
203
|
+
get visibleContainers() {
|
|
204
|
+
const components: ComponentRegistryInfo[] = [];
|
|
205
|
+
this.containersMap.forEach((component) => {
|
|
206
|
+
const state = component.options && this.state.get(component.options.containerId);
|
|
207
|
+
if (!state || !state.hidden) {
|
|
208
|
+
components.push(component);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
// TODO: 使用object来存state的话,初始containersMap为空,貌似就无法实现这个监听(无法引用到一个observable的属性)
|
|
212
|
+
const size = this.state.size; // 监听state长度
|
|
213
|
+
// 排序策略:默认根据priority来做一次排序,后续根据存储的index来排序,未存储过的(新插入的,比如插件)在渲染后(时序控制)始终放在最后
|
|
214
|
+
// 排序为 state的 priority 从小到大 (注意和 componentInfo 中的 options 的 priority的含义不同,为了不breaking change,保留这种语义)
|
|
215
|
+
return components.sort(
|
|
216
|
+
(pre, next) =>
|
|
217
|
+
this.getContainerState(pre.options!.containerId).priority -
|
|
218
|
+
this.getContainerState(next.options!.containerId).priority,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
registerResizeHandle(resizeHandle: ResizeHandle) {
|
|
223
|
+
const { setSize, setRelativeSize, getSize, getRelativeSize, lockSize, setMaxSize, hidePanel } = resizeHandle;
|
|
224
|
+
this.resizeHandle = {
|
|
225
|
+
setSize: (size) => setSize(size, this.isLatter),
|
|
226
|
+
setRelativeSize: (prev: number, next: number) => setRelativeSize(prev, next, this.isLatter),
|
|
227
|
+
getSize: () => getSize(this.isLatter),
|
|
228
|
+
getRelativeSize: () => getRelativeSize(this.isLatter),
|
|
229
|
+
setMaxSize: (lock: boolean | undefined) => setMaxSize(lock, this.isLatter),
|
|
230
|
+
lockSize: (lock: boolean | undefined) => lockSize(lock, this.isLatter),
|
|
231
|
+
hidePanel: (show) => hidePanel(show),
|
|
232
|
+
};
|
|
233
|
+
this.listenCurrentChange();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@action
|
|
237
|
+
registerContainer(containerId: string, componentInfo: ComponentRegistryInfo) {
|
|
238
|
+
if (this.containersMap.has(containerId)) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const disposables = new DisposableCollection();
|
|
242
|
+
let options = componentInfo.options;
|
|
243
|
+
if (!options) {
|
|
244
|
+
options = {
|
|
245
|
+
containerId,
|
|
246
|
+
};
|
|
247
|
+
componentInfo.options = options;
|
|
248
|
+
}
|
|
249
|
+
this.containersMap.set(containerId, {
|
|
250
|
+
views: componentInfo.views,
|
|
251
|
+
options: observable.object(options, undefined, { deep: false }),
|
|
252
|
+
});
|
|
253
|
+
disposables.push({
|
|
254
|
+
dispose: () => {
|
|
255
|
+
this.containersMap.delete(containerId);
|
|
256
|
+
this.state.delete(containerId);
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
this.updatePanelVisibility(this.containersMap.size > 0);
|
|
260
|
+
// 需要立刻设置state,lazy 逻辑会导致computed 的 visibleContainers 可能在计算时触发变更,抛出mobx invariant错误
|
|
261
|
+
// 另外由于containersMap不是observable, 这边setState来触发visibaleContainers更新
|
|
262
|
+
// 注册时直接根据priority排序,restoreState时恢复到记录的状态(对于集成侧在onDidStart之后注册的视图,不提供顺序状态恢复能力)
|
|
263
|
+
let insertIndex = this.sortedContainers.findIndex(
|
|
264
|
+
(item) => (item.options!.priority || 1) <= (componentInfo.options!.priority || 1),
|
|
265
|
+
);
|
|
266
|
+
if (insertIndex === -1) {
|
|
267
|
+
insertIndex = this.sortedContainers.length;
|
|
268
|
+
}
|
|
269
|
+
this.sortedContainers.splice(insertIndex, 0, componentInfo);
|
|
270
|
+
for (let i = insertIndex; i < this.sortedContainers.length; i++) {
|
|
271
|
+
const info = this.sortedContainers[i];
|
|
272
|
+
const containerId = info.options?.containerId;
|
|
273
|
+
if (containerId) {
|
|
274
|
+
const prevState = this.storedState[containerId] || this.getContainerState(containerId) || {}; // 保留原有的hidden状态
|
|
275
|
+
this.state.set(containerId, { hidden: prevState.hidden, priority: i });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
disposables.push(this.registerSideEffects(componentInfo));
|
|
279
|
+
this.eventBus.fire(new TabBarRegistrationEvent({ tabBarId: containerId }));
|
|
280
|
+
if (containerId === this.currentContainerId) {
|
|
281
|
+
// 需要重新触发currentChange副作用
|
|
282
|
+
this.handleChange(containerId, '');
|
|
283
|
+
}
|
|
284
|
+
this.viewContextKeyRegistry
|
|
285
|
+
.registerContextKeyService(containerId, this.contextKeyService.createScoped())
|
|
286
|
+
.createKey('view', containerId);
|
|
287
|
+
this.disposableMap.set(containerId, disposables);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
registerSideEffects(componentInfo: ComponentRegistryInfo) {
|
|
291
|
+
const disposables = new DisposableCollection();
|
|
292
|
+
if (!componentInfo.options?.hideTab) {
|
|
293
|
+
// 注册切换tab显隐的菜单
|
|
294
|
+
disposables.push(this.registerHideMenu(componentInfo));
|
|
295
|
+
// 注册溢出后的菜单
|
|
296
|
+
disposables.push(this.registerMoreMenu(componentInfo));
|
|
297
|
+
}
|
|
298
|
+
// 注册激活快捷键
|
|
299
|
+
disposables.push(this.registerActivateKeyBinding(componentInfo, componentInfo.options!.fromExtension));
|
|
300
|
+
// 注册视图是否存在的contextKey
|
|
301
|
+
const containerExistKey = this.contextKeyService.createKey<boolean>(
|
|
302
|
+
`workbench.${CONTAINER_NAME_MAP[this.location] || 'view'}.${componentInfo.options!.containerId}`,
|
|
303
|
+
true,
|
|
304
|
+
);
|
|
305
|
+
disposables.push({
|
|
306
|
+
dispose: () => {
|
|
307
|
+
containerExistKey.set(false);
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
// 注册progressIndicator
|
|
311
|
+
disposables.push(this.progressService.registerProgressIndicator(componentInfo.options!.containerId));
|
|
312
|
+
disposables.push(this.registerContainerPanelRelatedCommand(componentInfo));
|
|
313
|
+
return disposables;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* 这里注册的是某个 Container Panel 对应的显示/隐藏等命令
|
|
317
|
+
*/
|
|
318
|
+
protected registerContainerPanelRelatedCommand(componentInfo: ComponentRegistryInfo) {
|
|
319
|
+
const disposables = new DisposableCollection();
|
|
320
|
+
const containerId = componentInfo.options?.containerId!;
|
|
321
|
+
disposables.push(
|
|
322
|
+
this.commandRegistry.registerCommand(
|
|
323
|
+
{
|
|
324
|
+
id: 'container.show.' + containerId,
|
|
325
|
+
label: formatLocalize('view.command.show', componentInfo.options?.title ?? containerId),
|
|
326
|
+
alias: `Show ${componentInfo.options?.title ?? componentInfo.options?.containerId}`,
|
|
327
|
+
category: 'View',
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
execute: () => {
|
|
331
|
+
this.currentContainerId = containerId;
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
),
|
|
335
|
+
);
|
|
336
|
+
disposables.push(
|
|
337
|
+
this.commandRegistry.registerCommand(
|
|
338
|
+
{
|
|
339
|
+
id: 'container.hide.' + containerId,
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
execute: () => {
|
|
343
|
+
this.currentContainerId = '';
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
),
|
|
347
|
+
);
|
|
348
|
+
disposables.push(
|
|
349
|
+
this.commandRegistry.registerCommand(
|
|
350
|
+
{
|
|
351
|
+
id: 'container.toggle.' + containerId,
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
execute: () => {
|
|
355
|
+
if (this.currentContainerId === containerId) {
|
|
356
|
+
this.currentContainerId = '';
|
|
357
|
+
} else {
|
|
358
|
+
this.currentContainerId = containerId;
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
),
|
|
363
|
+
);
|
|
364
|
+
return disposables;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
protected registerHideMenu(componentInfo: ComponentRegistryInfo) {
|
|
368
|
+
const disposables = new DisposableCollection();
|
|
369
|
+
const containerId = componentInfo.options!.containerId;
|
|
370
|
+
|
|
371
|
+
disposables.push(
|
|
372
|
+
this.menuRegistry.registerMenuItem(this.menuId, {
|
|
373
|
+
command: {
|
|
374
|
+
id: this.registerVisibleToggleCommand(containerId, disposables),
|
|
375
|
+
label: componentInfo.options!.title || containerId,
|
|
376
|
+
},
|
|
377
|
+
group: '1_widgets',
|
|
378
|
+
}),
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
return disposables;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
protected registerMoreMenu(componentInfo: ComponentRegistryInfo) {
|
|
385
|
+
const disposables = new DisposableCollection();
|
|
386
|
+
const containerId = componentInfo.options!.containerId;
|
|
387
|
+
const tabInMoreKey = this.scopedCtxKeyService.createKey(this.getTabInMoreCtxKey(containerId), false);
|
|
388
|
+
this.tabInMoreKeyMap.set(containerId, tabInMoreKey);
|
|
389
|
+
disposables.push({
|
|
390
|
+
dispose: () => this.tabInMoreKeyMap.delete(containerId),
|
|
391
|
+
});
|
|
392
|
+
disposables.push(
|
|
393
|
+
this.menuRegistry.registerMenuItem(this.moreMenuId, {
|
|
394
|
+
command: {
|
|
395
|
+
id: this.registerMoreToggleCommand(componentInfo, disposables),
|
|
396
|
+
label: componentInfo.options!.title || containerId,
|
|
397
|
+
},
|
|
398
|
+
group: 'inline',
|
|
399
|
+
when: `${this.getTabInMoreCtxKey(containerId)} == true`,
|
|
400
|
+
toggledWhen: `${getTabbarCtxKey(this.location)} == ${containerId}`,
|
|
401
|
+
iconClass: componentInfo.options!.iconClass,
|
|
402
|
+
}),
|
|
403
|
+
);
|
|
404
|
+
return disposables;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
@action
|
|
408
|
+
disposeContainer(containerId: string) {
|
|
409
|
+
const disposables = this.disposableMap.get(containerId);
|
|
410
|
+
if (disposables) {
|
|
411
|
+
disposables.dispose();
|
|
412
|
+
}
|
|
413
|
+
if (this.currentContainerId === containerId) {
|
|
414
|
+
this.currentContainerId = this.visibleContainers[0].options!.containerId;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
getContainer(containerId: string) {
|
|
419
|
+
return this.containersMap.get(containerId);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// 针对 containerId 对 menu 进行缓存
|
|
423
|
+
private _menuMap = new Map<string, IMenu>();
|
|
424
|
+
|
|
425
|
+
public getTitleToolbarMenu(containerId: string) {
|
|
426
|
+
const existedMenu = this._menuMap.get(containerId);
|
|
427
|
+
if (existedMenu) {
|
|
428
|
+
return existedMenu;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const menu = this.menuService.createMenu(
|
|
432
|
+
MenuId.ViewTitle,
|
|
433
|
+
this.viewContextKeyRegistry.getContextKeyService(containerId),
|
|
434
|
+
);
|
|
435
|
+
this._menuMap.set(containerId, menu);
|
|
436
|
+
|
|
437
|
+
// 添加到 containerId 对应的 disposable 中
|
|
438
|
+
// 在 containerId 对应的 view dispose 时会将 menu 对象和缓存清理掉
|
|
439
|
+
const disposables = this.disposableMap.get(containerId);
|
|
440
|
+
const toDispose = [
|
|
441
|
+
menu,
|
|
442
|
+
toDisposable(() => {
|
|
443
|
+
this._menuMap.delete(containerId);
|
|
444
|
+
}),
|
|
445
|
+
];
|
|
446
|
+
|
|
447
|
+
if (disposables) {
|
|
448
|
+
disposables.pushAll(toDispose);
|
|
449
|
+
} else {
|
|
450
|
+
// 避免出现不必要的内存泄露
|
|
451
|
+
this.addDispose(toDispose);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return menu;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
doExpand(expand: boolean) {
|
|
458
|
+
if (this.resizeHandle) {
|
|
459
|
+
const { setRelativeSize } = this.resizeHandle;
|
|
460
|
+
if (expand) {
|
|
461
|
+
if (!this.isLatter) {
|
|
462
|
+
setRelativeSize(1, 0);
|
|
463
|
+
} else {
|
|
464
|
+
setRelativeSize(0, 1);
|
|
465
|
+
}
|
|
466
|
+
} else {
|
|
467
|
+
// FIXME 底部需要额外的字段记录展开前的尺寸
|
|
468
|
+
setRelativeSize(2, 1);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
get isExpanded(): boolean {
|
|
474
|
+
if (this.resizeHandle) {
|
|
475
|
+
const { getRelativeSize } = this.resizeHandle;
|
|
476
|
+
const relativeSizes = getRelativeSize().join(',');
|
|
477
|
+
return this.isLatter ? relativeSizes === '0,1' : relativeSizes === '1,0';
|
|
478
|
+
}
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
@action.bound handleTabClick(e: React.MouseEvent, forbidCollapse?: boolean) {
|
|
483
|
+
const containerId = e.currentTarget.id;
|
|
484
|
+
if (containerId === this.currentContainerId && !forbidCollapse) {
|
|
485
|
+
// 双击同一个 tab 时隐藏 panel
|
|
486
|
+
this.currentContainerId = '';
|
|
487
|
+
} else {
|
|
488
|
+
this.currentContainerId = containerId;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
@action.bound handleContextMenu(event: React.MouseEvent, containerId?: string) {
|
|
493
|
+
event.preventDefault();
|
|
494
|
+
event.stopPropagation();
|
|
495
|
+
const menus = this.menuService.createMenu(
|
|
496
|
+
this.menuId,
|
|
497
|
+
containerId ? this.scopedCtxKeyService : this.contextKeyService,
|
|
498
|
+
);
|
|
499
|
+
const menuNodes = generateCtxMenu({ menus, args: [{ containerId }] });
|
|
500
|
+
this.contextMenuRenderer.show({
|
|
501
|
+
menuNodes: menuNodes[1],
|
|
502
|
+
anchor: {
|
|
503
|
+
x: event.clientX,
|
|
504
|
+
y: event.clientY,
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
showMoreMenu(event: React.MouseEvent, lastContainerId?: string) {
|
|
510
|
+
const menus = this.menuService.createMenu(this.moreMenuId, this.scopedCtxKeyService);
|
|
511
|
+
const menuNodes = generateCtxMenu({ menus, args: [{ lastContainerId }] });
|
|
512
|
+
this.contextMenuRenderer.show({
|
|
513
|
+
menuNodes: menuNodes[1],
|
|
514
|
+
anchor: {
|
|
515
|
+
x: event.clientX,
|
|
516
|
+
y: event.clientY,
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// drag & drop
|
|
522
|
+
handleDragStart(e: React.DragEvent, containerId: string) {
|
|
523
|
+
e.dataTransfer.setData('containerId', containerId);
|
|
524
|
+
}
|
|
525
|
+
handleDrop(e: React.DragEvent, target: string) {
|
|
526
|
+
if (e.dataTransfer.getData('containerId')) {
|
|
527
|
+
const source = e.dataTransfer.getData('containerId');
|
|
528
|
+
const containers = this.visibleContainers;
|
|
529
|
+
const sourceIndex = containers.findIndex((containerInfo) => source === containerInfo.options!.containerId);
|
|
530
|
+
const targetIndex = containers.findIndex((containerInfo) => target === containerInfo.options!.containerId);
|
|
531
|
+
this.doInsertTab(containers, sourceIndex, targetIndex);
|
|
532
|
+
this.storeState();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
restoreState() {
|
|
537
|
+
this.storedState = this.layoutState.getState(LAYOUT_STATE.getTabbarSpace(this.location), {});
|
|
538
|
+
for (const containerId of this.state.keys()) {
|
|
539
|
+
if (this.storedState[containerId]) {
|
|
540
|
+
this.state.set(containerId, this.storedState[containerId]);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
this.visibleContainers.forEach((container) => {
|
|
544
|
+
if (SUPPORT_ACCORDION_LOCATION.has(this.location)) {
|
|
545
|
+
this.tryRestoreAccordionSize(container.options!.containerId);
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
protected doInsertTab(containers: ComponentRegistryInfo[], sourceIndex: number, targetIndex: number) {
|
|
551
|
+
const targetPriority = this.getContainerState(containers[targetIndex].options!.containerId).priority;
|
|
552
|
+
const changePriority = (sourceIndex: number, targetIndex: number) => {
|
|
553
|
+
const sourceState = this.getContainerState(containers[sourceIndex].options!.containerId);
|
|
554
|
+
const targetState = this.getContainerState(containers[targetIndex].options!.containerId);
|
|
555
|
+
sourceState.priority = targetState.priority;
|
|
556
|
+
};
|
|
557
|
+
let index: number;
|
|
558
|
+
if (sourceIndex > targetIndex) {
|
|
559
|
+
// 后往前拖,中间的tab依次下移
|
|
560
|
+
for (index = targetIndex; index < sourceIndex; index++) {
|
|
561
|
+
changePriority(index, index + 1);
|
|
562
|
+
}
|
|
563
|
+
} else {
|
|
564
|
+
// 前往后拖,中间的上移
|
|
565
|
+
for (index = targetIndex; index > sourceIndex; index--) {
|
|
566
|
+
changePriority(index, index - 1);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
this.getContainerState(containers[index].options!.containerId).priority = targetPriority;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
protected storeState() {
|
|
573
|
+
const stateObj = {};
|
|
574
|
+
this.state.forEach((value, key) => {
|
|
575
|
+
stateObj[key] = value;
|
|
576
|
+
});
|
|
577
|
+
this.layoutState.setState(LAYOUT_STATE.getTabbarSpace(this.location), stateObj);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// 注册Tab的激活快捷键,对于底部panel,为切换快捷键
|
|
581
|
+
private registerActivateKeyBinding(component: ComponentRegistryInfo, fromExtension?: boolean) {
|
|
582
|
+
const options = component.options!;
|
|
583
|
+
const containerId = options.containerId;
|
|
584
|
+
// vscode内插件注册的是workbench.view.extension.containerId
|
|
585
|
+
const activateCommandId = fromExtension
|
|
586
|
+
? `workbench.view.extension.${containerId}`
|
|
587
|
+
: `workbench.view.${containerId}`;
|
|
588
|
+
const disposables = new DisposableCollection();
|
|
589
|
+
disposables.push(
|
|
590
|
+
this.commandRegistry.registerCommand(
|
|
591
|
+
{
|
|
592
|
+
id: activateCommandId,
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
execute: ({ forceShow }: { forceShow?: boolean } = {}) => {
|
|
596
|
+
// 支持toggle
|
|
597
|
+
if (this.location === 'bottom' && !forceShow) {
|
|
598
|
+
this.currentContainerId = this.currentContainerId === containerId ? '' : containerId;
|
|
599
|
+
} else {
|
|
600
|
+
this.currentContainerId = containerId;
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
),
|
|
605
|
+
);
|
|
606
|
+
if (options.activateKeyBinding) {
|
|
607
|
+
disposables.push(
|
|
608
|
+
this.keybindingRegistry.registerKeybinding({
|
|
609
|
+
command: activateCommandId,
|
|
610
|
+
keybinding: options.activateKeyBinding!,
|
|
611
|
+
}),
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
return disposables;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private registerGlobalToggleCommand() {
|
|
618
|
+
const commandId = `activity.bar.toggle.${this.location}`;
|
|
619
|
+
this.commandRegistry.registerCommand(
|
|
620
|
+
{
|
|
621
|
+
id: commandId,
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
execute: ({ containerId }: { containerId: string }) => {
|
|
625
|
+
this.doToggleTab(containerId);
|
|
626
|
+
},
|
|
627
|
+
isEnabled: () => this.visibleContainers.length > 1,
|
|
628
|
+
},
|
|
629
|
+
);
|
|
630
|
+
return commandId;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// 注册当前 container 在 tabBar 上的隐藏显示功能
|
|
634
|
+
private registerVisibleToggleCommand(containerId: string, disposables: DisposableCollection): string {
|
|
635
|
+
const commandId = `activity.bar.toggle.${containerId}`;
|
|
636
|
+
disposables.push(
|
|
637
|
+
this.commandRegistry.registerCommand(
|
|
638
|
+
{
|
|
639
|
+
id: commandId,
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
execute: ({ forceShow }: { forceShow?: boolean } = {}) => {
|
|
643
|
+
this.doToggleTab(containerId, forceShow);
|
|
644
|
+
},
|
|
645
|
+
isToggled: () => {
|
|
646
|
+
const state = this.getContainerState(containerId);
|
|
647
|
+
return !state.hidden;
|
|
648
|
+
},
|
|
649
|
+
isEnabled: () => {
|
|
650
|
+
const state = this.getContainerState(containerId);
|
|
651
|
+
return state.hidden || this.visibleContainers.length !== 1;
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
),
|
|
655
|
+
);
|
|
656
|
+
return commandId;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
private registerMoreToggleCommand(component: ComponentRegistryInfo, disposables: DisposableCollection): string {
|
|
660
|
+
const { options } = component;
|
|
661
|
+
const { containerId } = options!;
|
|
662
|
+
const commandId = `activity.bar.activate.more.${containerId}`;
|
|
663
|
+
disposables.push(
|
|
664
|
+
this.commandRegistry.registerCommand(
|
|
665
|
+
{
|
|
666
|
+
id: commandId,
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
execute: ({ lastContainerId }: { lastContainerId?: string }) => {
|
|
670
|
+
// 切换激活tab
|
|
671
|
+
this.currentContainerId = containerId;
|
|
672
|
+
if (lastContainerId) {
|
|
673
|
+
// 替换最后一个可见tab
|
|
674
|
+
const sourceState = this.getContainerState(containerId);
|
|
675
|
+
const targetState = this.getContainerState(lastContainerId);
|
|
676
|
+
const sourcePriority = sourceState.priority;
|
|
677
|
+
sourceState.priority = targetState.priority;
|
|
678
|
+
targetState.priority = sourcePriority;
|
|
679
|
+
this.storeState();
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
},
|
|
683
|
+
),
|
|
684
|
+
);
|
|
685
|
+
return commandId;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
private getTabInMoreCtxKey(containerId: string) {
|
|
689
|
+
return `${containerId}.isInMore`;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
protected registerPanelMenus() {
|
|
693
|
+
this.menuRegistry.registerMenuItems(getTabbarCommonMenuId('bottom'), [
|
|
694
|
+
{
|
|
695
|
+
command: EXPAND_BOTTOM_PANEL.id,
|
|
696
|
+
group: 'navigation',
|
|
697
|
+
when: '!bottomFullExpanded',
|
|
698
|
+
order: 1,
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
command: RETRACT_BOTTOM_PANEL.id,
|
|
702
|
+
group: 'navigation',
|
|
703
|
+
when: 'bottomFullExpanded',
|
|
704
|
+
order: 1,
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
command: TOGGLE_BOTTOM_PANEL_COMMAND.id,
|
|
708
|
+
group: 'navigation',
|
|
709
|
+
order: 2,
|
|
710
|
+
},
|
|
711
|
+
]);
|
|
712
|
+
this.commonTitleMenu = this.ctxMenuService.createMenu({
|
|
713
|
+
id: 'tabbar/bottom/common',
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
protected doToggleTab(containerId: string, forceShow?: boolean) {
|
|
718
|
+
const state = this.getContainerState(containerId);
|
|
719
|
+
if (forceShow === undefined) {
|
|
720
|
+
state.hidden = !state.hidden;
|
|
721
|
+
} else {
|
|
722
|
+
state.hidden = !forceShow;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (state.hidden && this.currentContainerId === containerId) {
|
|
726
|
+
// 如果隐藏的是当前激活的 tab,则激活第一个可见 tab
|
|
727
|
+
this.currentContainerId = this.visibleContainers[0].options!.containerId;
|
|
728
|
+
}
|
|
729
|
+
this.storeState();
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
protected shouldExpand(containerId: string) {
|
|
733
|
+
const info = this.getContainer(containerId);
|
|
734
|
+
return info && info.options && info.options.expanded;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
@OnEvent(ResizeEvent)
|
|
738
|
+
protected onResize(e: ResizeEvent) {
|
|
739
|
+
if (e.payload.slotLocation === this.location) {
|
|
740
|
+
if (!this.currentContainerId || !this.resizeHandle) {
|
|
741
|
+
// 折叠时不监听变化
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
const size = this.resizeHandle.getSize();
|
|
745
|
+
if (size !== this.barSize && !this.shouldExpand(this.currentContainerId)) {
|
|
746
|
+
this.prevSize = size;
|
|
747
|
+
this.onSizeChangeEmitter.fire({ size });
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
protected listenCurrentChange() {
|
|
753
|
+
observe(this, 'currentContainerId', (change) => {
|
|
754
|
+
if (this.prevSize === undefined) {
|
|
755
|
+
}
|
|
756
|
+
this.previousContainerId = change.oldValue || '';
|
|
757
|
+
const currentId = change.newValue;
|
|
758
|
+
this.handleChange(currentId, this.previousContainerId);
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
private handleChange(currentId, previousId) {
|
|
763
|
+
// 这里的 handleChange 是会在 registerResizeHandle 后才会执行
|
|
764
|
+
// 这里的判断只是防御行为
|
|
765
|
+
if (!this.resizeHandle) {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const { getSize, setSize, lockSize } = this.resizeHandle;
|
|
769
|
+
this.onCurrentChangeEmitter.fire({ previousId, currentId });
|
|
770
|
+
const isCurrentExpanded = this.shouldExpand(currentId);
|
|
771
|
+
if (this.shouldExpand(this.previousContainerId) || isCurrentExpanded) {
|
|
772
|
+
this.handleFullExpanded(currentId, isCurrentExpanded);
|
|
773
|
+
} else {
|
|
774
|
+
if (currentId) {
|
|
775
|
+
if (previousId && currentId !== previousId) {
|
|
776
|
+
this.prevSize = getSize();
|
|
777
|
+
}
|
|
778
|
+
const containerInfo = this.getContainer(currentId);
|
|
779
|
+
|
|
780
|
+
setSize(this.prevSize || this.panelSize + this.barSize);
|
|
781
|
+
lockSize(Boolean(containerInfo?.options?.noResize));
|
|
782
|
+
|
|
783
|
+
this.activatedKey.set(currentId);
|
|
784
|
+
} else {
|
|
785
|
+
setSize(this.barSize);
|
|
786
|
+
lockSize(true);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
protected tryRestoreAccordionSize(containerId: string) {
|
|
792
|
+
if (this.accordionRestored.has(containerId)) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
const containerInfo = this.containersMap.get(containerId);
|
|
796
|
+
// 使用自定义视图取代手风琴的面板不需要 restore
|
|
797
|
+
// scm 视图例外,因为在新版本 Gitlens 中可以将自己注册到 scm 中
|
|
798
|
+
// 暂时用这种方式使 scm 面板状态可以被持久化
|
|
799
|
+
if (
|
|
800
|
+
(!containerInfo || containerInfo.options?.component) &&
|
|
801
|
+
containerInfo?.options?.containerId !== SCM_CONTAINER_ID
|
|
802
|
+
) {
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const accordionService = this.layoutService.getAccordionService(containerId);
|
|
806
|
+
// 需要保证此时tab切换已完成dom渲染
|
|
807
|
+
accordionService.restoreState();
|
|
808
|
+
this.accordionRestored.add(containerId);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
protected handleFullExpanded(currentId: string, isCurrentExpanded?: boolean) {
|
|
812
|
+
if (!this.resizeHandle) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const { setRelativeSize, setSize } = this.resizeHandle;
|
|
816
|
+
if (currentId) {
|
|
817
|
+
if (isCurrentExpanded) {
|
|
818
|
+
if (!this.isLatter) {
|
|
819
|
+
setRelativeSize(1, 0);
|
|
820
|
+
} else {
|
|
821
|
+
setRelativeSize(0, 1);
|
|
822
|
+
}
|
|
823
|
+
} else {
|
|
824
|
+
setSize(this.prevSize || this.panelSize + this.barSize);
|
|
825
|
+
}
|
|
826
|
+
} else {
|
|
827
|
+
setSize(this.barSize);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function visibleContainerEquals(a: ComponentRegistryInfo[], b: ComponentRegistryInfo[]): boolean {
|
|
833
|
+
if (a.length !== b.length) {
|
|
834
|
+
return false;
|
|
835
|
+
} else {
|
|
836
|
+
let isEqual = true;
|
|
837
|
+
for (let i = 0; i < a.length; i++) {
|
|
838
|
+
if (a[i] !== b[i]) {
|
|
839
|
+
isEqual = false;
|
|
840
|
+
break;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return isEqual;
|
|
844
|
+
}
|
|
845
|
+
}
|