TabBarToolbar Items attach to all widgets

I am using the scm extension (scm-frontend-module, scm-contribution) as a reference in creating a new widget. I chose this extension to reference because it has a nice TabBarToolbar and tree view that I wanted to replicate.

The problem I have is that the TabBarToolbarItems are being added to every widget, rather than only the intended widget. This picture shows the SCM widget with extra unintended “toggle tree view” and “list view” items:

Screenshot_20210105_213401

My feeling is that it has something to do with the container that the widget is binding to, but I haven’t been able to locate the issue. My structure is as follows:

my-frontend-module:

bind(MyWidget).toSelf()

bind(WidgetFactory).toDynamicValue(ctx => ({
    id: MyWidgetFactoryId,
    createWidget: (options: MyWidgetOpts) => {
        const child = new Container({ defaultScope: 'Singleton' });
        child.parent = ctx.container;
        child.bind(MyWidgetOpts).toConstantValue(options)
        return child.get(MyWidget)
    }
})).inSingletonScope();

bindViewContribution(bind, MyWidgetContribution)
for (const identifier of [CommandContribution, MenuContribution, TabBarToolbarContribution]) {
    bind(identifier).toService(MyWidgetContribution);
}

bind(WidgetFactory).toDynamicValue(({ container }) => ({
    id: MyContainerId,
    createWidget: async () => {
        const viewContainer = container.get<ViewContainer.Factory>(ViewContainer.Factory)({
            id: MyContainerId,
            progressLocationId: 'my-widget'
        });
        viewContainer.setTitleOptions(MyContainerTitleOpts);
        const widget = await container.get(WidgetManager).getOrCreateWidget(MyWidgetFactoryId);
        viewContainer.addWidget(widget, {
            canHide: false,
            initiallyCollapsed: false
        });
        return viewContainer;
    }
})).inSingletonScope();

my-widget-contribution:

@injectable()
export class MyWidgetContribution extends AbstractViewContribution<MyWidget>
implements FrontendApplicationContribution, TabBarToolbarContribution {

@inject(ApplicationShell) protected readonly shell: ApplicationShell;
@inject(WidgetManager) protected readonly widgetManager: WidgetManager;
@inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry;
@inject(ContextKeyService) protected readonly contextKeys: ContextKeyService;

readonly currentEditor: MyWidget | undefined;

protected myFocus: ContextKey<boolean>;

constructor() {
    super({
        defaultWidgetOptions: { area: "left", rank: 300 },
        toggleCommandId: "my:toggle",
        toggleKeybinding: "ctrlcmd+shift+m",
        viewContainerId: MyContainerId,
        widgetName: "My Widget",
        widgetId: MyWidgetFactoryId,
    });
}

@postConstruct()
protected init(): void {
    this. myFocus = this.contextKeys.createKey('myFocus', false);
}

async initializeLayout(): Promise<void> {
    await this.openView();
}

onStart(): void {
    this.updateContextKeys()
    this.shell.onDidChangeCurrentWidget(() => this.updateContextKeys())
}

protected updateContextKeys(): void {
    this.myFocus.set(this.shell.currentWidget instanceof MyWidget);
}

registerToolbarItems(registry: TabBarToolbarRegistry): void {
    const viewModeEmitter = new Emitter<void>();
    const extractEditorWidget = (widget: any) => {
        if (widget instanceof ViewContainer) {
            const layout = widget.containerLayout;
            const myWidgetPart = find(layout.iter(), part => part.wrapped instanceof MyWidget);
            if (myWidgetPart && myWidgetPart.wrapped instanceof MyWidget) {
                return myWidgetPart.wrapped;
            }
        }
    }

    const registerToggleViewItem = (command: Command, mode: 'tree' | 'list') => {
        const id = command.id;
        const item: TabBarToolbarItem = {
            id,
            command: id,
            tooltip: command.label,
            onDidChange: viewModeEmitter.event
        };
        this.commandRegistry.registerCommand({ id, iconClass: command && command.iconClass }, {
            execute: widget => {
            },
            isVisible: widget => {
                return true;
            },
        });
        registry.registerItem(item);
    };
    registerToggleViewItem(MyCommands.TREE_VIEW_MODE, 'tree');
    registerToggleViewItem(MyCommands.LIST_VIEW_MODE, 'list');

    this.commandRegistry.registerCommand(MyCommands.COLLAPSE_ALL, {
        execute: widget => {

        },
        isVisible: widget => {
            return true;
        }
    });

    registry.registerItem({
        ...MyCommands.COLLAPSE_ALL,
        command: MyCommands.COLLAPSE_ALL.id
    });
}
}

The menu item in the “View” drop-down menu also lists my widget twice. Any ideas as to what I may be missing? I am using theia v1.6.0

Some more data. I figured out the double menu item in the “View” drop-down: Inheriting from FrontendApplicationContribution already binds the command and menu contributions to my contribution, so I was doing it twice.

Still haven’t figured out the toolbar item replication. Printing the toolbar registry I did see this:

1. 0: "scm.viewmode.tree"
2. 1: "scm.viewmode.list"
3. 2: "my-widget.viewmode.tree"
4. 3: "__explorer-view-container_title:my-widget.viewmode.tree"
5. 4: "__my-widget-container_title:my-widget.viewmode.tree"
6. 5: "__scm-view-container_title:my-widget.viewmode.tree"

I’m not sure what the “__” refers to, but it seems like I must be binding something to a super class or not narrowing the scope of the item - not sure how to do this.

@shortwavedave in order for you not to have icons for each view, you’ll need to implement a withWidget and use it when you enable, set visible the commands that are present in your toolbar. For example, the output view has (for the lock command):

You’ll see this pattern for multiple views.

Thank you @vince-fugnitto. That fixed it!

1 Like