mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-17 23:35:54 +01:00
quickinput: keep Command Center visible when Quick Pick is dragged away (#306139)
* quickinput: expose alignment observable, keep Command Center visible when Quick Pick is dragged
When the Quick Pick is dragged away from its default top position, the
Command Center in the title bar no longer hides. Previously it always
hid on Quick Pick show, even when the widget was in a custom position
where it wouldn't overlap.
Changes:
- Add QuickInputAlignment type ('top' | 'center' | 'custom') and
alignment observable to IQuickInputService
- DnD controller tracks alignment via _setAlignmentState() helper that
updates both the context key and the observable
- Each service layer has a stable observableValue mirrored via autorun
(avoids breaking subscriptions on lazy controller creation)
- Command Center uses autorun to reactively show/hide based on alignment
- Fix onShowEmitter.fire() timing (moved after layoutContainer so
alignment is settled before listeners fire)
- Fix top===0 truthiness bugs in layoutContainer and updateLayout
- Fix double-click reset not updating alignment state
Fixes #306138
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* quickinput: handle anchored inputs and partial state in alignment
- Set alignment to 'custom' for anchored quick inputs (positioned near
a DOM element, not at the top)
- Re-sync alignment from DnD controller on non-anchored show to prevent
stale 'custom' value after an anchored input closes
- Guard setAlignment() to no-op while an anchored input is visible
- DnD alignment typed as IObservable<QuickInputAlignment> (read-only)
- Require both top and left in persisted state before marking as custom
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -102,6 +102,7 @@ export class StandaloneQuickInputService implements IQuickInputService {
|
||||
get currentQuickInput() { return this.activeService.currentQuickInput; }
|
||||
get quickAccess() { return this.activeService.quickAccess; }
|
||||
get backButton() { return this.activeService.backButton; }
|
||||
get alignment() { return this.activeService.alignment; }
|
||||
get onShow() { return this.activeService.onShow; }
|
||||
get onHide() { return this.activeService.onHide; }
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import Severity from '../../../base/common/severity.js';
|
||||
import { isString } from '../../../base/common/types.js';
|
||||
import { isModifierKey } from '../../../base/common/keyCodes.js';
|
||||
import { localize } from '../../../nls.js';
|
||||
import { IInputBox, IInputOptions, IKeyMods, IPickOptions, IQuickInput, IQuickInputButton, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, IQuickWidget, QuickInputHideReason, QuickPickInput, QuickPickFocus, QuickInputType, IQuickTree, IQuickTreeItem } from '../common/quickInput.js';
|
||||
import { IInputBox, IInputOptions, IKeyMods, IPickOptions, IQuickInput, IQuickInputButton, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, IQuickWidget, QuickInputHideReason, QuickPickInput, QuickPickFocus, QuickInputType, IQuickTree, IQuickTreeItem, QuickInputAlignment } from '../common/quickInput.js';
|
||||
import { QuickInputBox } from './quickInputBox.js';
|
||||
import { QuickInputUI, Writeable, IQuickInputStyles, IQuickInputOptions, QuickPick, backButton, InputBox, Visibilities, QuickWidget, InQuickInputContextKey, QuickInputTypeContextKey, EndOfQuickInputBoxContextKey, QuickInputAlignmentContextKey } from './quickInput.js';
|
||||
import { ILayoutService } from '../../layout/browser/layoutService.js';
|
||||
@@ -26,7 +26,7 @@ import { IContextMenuService } from '../../contextview/browser/contextView.js';
|
||||
import { QuickInputList } from './quickInputList.js';
|
||||
import { IContextKey, IContextKeyService } from '../../contextkey/common/contextkey.js';
|
||||
import './quickInputActions.js';
|
||||
import { autorun, observableValue } from '../../../base/common/observable.js';
|
||||
import { IObservable, autorun, observableValue } from '../../../base/common/observable.js';
|
||||
import { StandardMouseEvent } from '../../../base/browser/mouseEvent.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js';
|
||||
import { IConfigurationService } from '../../configuration/common/configuration.js';
|
||||
@@ -81,6 +81,9 @@ export class QuickInputController extends Disposable {
|
||||
private viewState: QuickInputViewState | undefined;
|
||||
private dndController: QuickInputDragAndDropController | undefined;
|
||||
|
||||
private readonly _alignment = observableValue<QuickInputAlignment>(this, 'top');
|
||||
readonly alignment: IObservable<QuickInputAlignment> = this._alignment;
|
||||
|
||||
private readonly inQuickInputContext: IContextKey<boolean>;
|
||||
private readonly quickInputTypeContext: IContextKey<QuickInputType>;
|
||||
private readonly endOfQuickInputBoxContext: IContextKey<boolean>;
|
||||
@@ -401,6 +404,11 @@ export class QuickInputController extends Disposable {
|
||||
}
|
||||
}));
|
||||
|
||||
// Mirror DnD alignment into the stable observable
|
||||
this._register(autorun(reader => {
|
||||
this._alignment.set(this.dndController!.alignment.read(reader), undefined);
|
||||
}));
|
||||
|
||||
this.ui = {
|
||||
container,
|
||||
styleSheet,
|
||||
@@ -656,6 +664,9 @@ export class QuickInputController extends Disposable {
|
||||
}
|
||||
|
||||
setAlignment(alignment: 'top' | 'center' | { top: number; left: number }): void {
|
||||
if (this.controller?.anchor) {
|
||||
return; // anchored inputs own their own positioning
|
||||
}
|
||||
this.dndController?.setAlignment(alignment);
|
||||
}
|
||||
|
||||
@@ -671,7 +682,6 @@ export class QuickInputController extends Disposable {
|
||||
|
||||
private show(controller: IQuickInput) {
|
||||
const ui = this.getUI(true);
|
||||
this.onShowEmitter.fire();
|
||||
const oldController = this.controller;
|
||||
this.controller = controller;
|
||||
oldController?.didHide();
|
||||
@@ -716,6 +726,15 @@ export class QuickInputController extends Disposable {
|
||||
this.updateLayout();
|
||||
this.dndController?.setEnabled(!controller.anchor);
|
||||
this.dndController?.layoutContainer();
|
||||
if (controller.anchor) {
|
||||
// Anchored quick inputs are positioned near a specific element, not
|
||||
// at the default top location, so report them as custom-positioned.
|
||||
this._alignment.set('custom', undefined);
|
||||
} else {
|
||||
// Re-sync from DnD in case a previous anchored input left us stale.
|
||||
this._alignment.set(this.dndController?.alignment.get() ?? 'top', undefined);
|
||||
}
|
||||
this.onShowEmitter.fire();
|
||||
ui.inputBox.setFocus();
|
||||
this.quickInputTypeContext.set(controller.type);
|
||||
}
|
||||
@@ -903,7 +922,7 @@ export class QuickInputController extends Disposable {
|
||||
style.width = `${width}px`;
|
||||
style.height = '';
|
||||
} else {
|
||||
style.top = `${this.viewState?.top ? Math.round(this.dimension!.height * this.viewState.top) : this.titleBarOffset}px`;
|
||||
style.top = `${this.viewState?.top !== undefined ? Math.round(this.dimension!.height * this.viewState.top) : this.titleBarOffset}px`;
|
||||
style.left = `${Math.round((this.dimension!.width * (this.viewState?.left ?? 0.5 /* center */)) - (width / 2))}px`;
|
||||
style.right = '';
|
||||
style.bottom = '';
|
||||
@@ -1014,7 +1033,9 @@ class QuickInputDragAndDropController extends Disposable {
|
||||
private readonly _controlsOnLeft: boolean;
|
||||
private readonly _controlsOnRight: boolean;
|
||||
|
||||
private _quickInputAlignmentContext: IContextKey<'center' | 'top' | undefined>;
|
||||
private readonly _quickInputAlignmentContext: IContextKey<'center' | 'top' | undefined>;
|
||||
private readonly _alignment = observableValue<QuickInputAlignment>(this, 'top');
|
||||
readonly alignment: IObservable<QuickInputAlignment> = this._alignment;
|
||||
|
||||
constructor(
|
||||
private _container: HTMLElement,
|
||||
@@ -1036,6 +1057,11 @@ class QuickInputDragAndDropController extends Disposable {
|
||||
this._registerLayoutListener();
|
||||
this.registerMouseListeners();
|
||||
this.dndViewState.set({ ...initialViewState, done: true }, undefined);
|
||||
// Initialize alignment from restored state. The exact snap alignment will
|
||||
// be refined in layoutContainer() once pixel dimensions are available.
|
||||
if (initialViewState?.top !== undefined && initialViewState?.left !== undefined) {
|
||||
this._setAlignmentState(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
reparentUI(container: HTMLElement): void {
|
||||
@@ -1049,7 +1075,7 @@ class QuickInputDragAndDropController extends Disposable {
|
||||
|
||||
const state = this.dndViewState.get();
|
||||
const dragAreaRect = this._quickInputContainer.getBoundingClientRect();
|
||||
if (state?.top && state?.left) {
|
||||
if (state?.top !== undefined && state?.left !== undefined) {
|
||||
const a = Math.round(state.left * 1e2) / 1e2;
|
||||
const b = dimension.width;
|
||||
const c = dragAreaRect.width;
|
||||
@@ -1063,6 +1089,11 @@ class QuickInputDragAndDropController extends Disposable {
|
||||
this._quickInputContainer.classList.toggle('no-drag', !enabled);
|
||||
}
|
||||
|
||||
private _setAlignmentState(value: 'top' | 'center' | undefined): void {
|
||||
this._quickInputAlignmentContext.set(value);
|
||||
this._alignment.set(value ?? 'custom', undefined);
|
||||
}
|
||||
|
||||
setAlignment(alignment: 'top' | 'center' | { top: number; left: number }, done = true): void {
|
||||
if (alignment === 'top') {
|
||||
this.dndViewState.set({
|
||||
@@ -1070,17 +1101,17 @@ class QuickInputDragAndDropController extends Disposable {
|
||||
left: (this._getCenterXSnapValue() + (this._quickInputContainer.clientWidth / 2)) / this._container.clientWidth,
|
||||
done
|
||||
}, undefined);
|
||||
this._quickInputAlignmentContext.set('top');
|
||||
this._setAlignmentState('top');
|
||||
} else if (alignment === 'center') {
|
||||
this.dndViewState.set({
|
||||
top: this._getCenterYSnapValue() / this._container.clientHeight,
|
||||
left: (this._getCenterXSnapValue() + (this._quickInputContainer.clientWidth / 2)) / this._container.clientWidth,
|
||||
done
|
||||
}, undefined);
|
||||
this._quickInputAlignmentContext.set('center');
|
||||
this._setAlignmentState('center');
|
||||
} else {
|
||||
this.dndViewState.set({ top: alignment.top, left: alignment.left, done }, undefined);
|
||||
this._quickInputAlignmentContext.set(undefined);
|
||||
this._setAlignmentState(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1109,6 +1140,7 @@ class QuickInputDragAndDropController extends Disposable {
|
||||
}
|
||||
|
||||
this.dndViewState.set({ top: undefined, left: undefined, done: true }, undefined);
|
||||
this._setAlignmentState('top');
|
||||
}));
|
||||
|
||||
// Mouse down
|
||||
@@ -1190,14 +1222,14 @@ class QuickInputDragAndDropController extends Disposable {
|
||||
this.dndViewState.set({ top, left, done: false }, undefined);
|
||||
if (snappingToCenterX) {
|
||||
if (snappingToTop) {
|
||||
this._quickInputAlignmentContext.set('top');
|
||||
this._setAlignmentState('top');
|
||||
return;
|
||||
} else if (snappingToCenter) {
|
||||
this._quickInputAlignmentContext.set('center');
|
||||
this._setAlignmentState('center');
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._quickInputAlignmentContext.set(undefined);
|
||||
this._setAlignmentState(undefined);
|
||||
}
|
||||
|
||||
private _getTopSnapValue() {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ILayoutService } from '../../layout/browser/layoutService.js';
|
||||
import { IOpenerService } from '../../opener/common/opener.js';
|
||||
import { QuickAccessController } from './quickAccess.js';
|
||||
import { IQuickAccessController } from '../common/quickAccess.js';
|
||||
import { IInputBox, IInputOptions, IKeyMods, IPickOptions, IQuickInputButton, IQuickInputService, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, IQuickTree, IQuickTreeItem, IQuickWidget, QuickInputHideReason, QuickPickInput } from '../common/quickInput.js';
|
||||
import { IInputBox, IInputOptions, IKeyMods, IPickOptions, IQuickInputButton, IQuickInputService, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, IQuickTree, IQuickTreeItem, IQuickWidget, QuickInputAlignment, QuickInputHideReason, QuickPickInput } from '../common/quickInput.js';
|
||||
import { defaultButtonStyles, defaultCountBadgeStyles, defaultInputBoxStyles, defaultKeybindingLabelStyles, defaultProgressBarStyles, defaultToggleStyles, getListStyles } from '../../theme/browser/defaultStyles.js';
|
||||
import { activeContrastBorder, asCssVariable, pickerGroupBorder, pickerGroupForeground, quickInputBackground, quickInputForeground, quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, quickInputTitleBackground, widgetBorder, widgetShadow } from '../../theme/common/colorRegistry.js';
|
||||
import { IThemeService, Themable } from '../../theme/common/themeService.js';
|
||||
@@ -19,6 +19,7 @@ import { IQuickInputOptions, IQuickInputStyles, QuickInputHoverDelegate } from '
|
||||
import { QuickInputController, IQuickInputControllerHost } from './quickInputController.js';
|
||||
import { IConfigurationService } from '../../configuration/common/configuration.js';
|
||||
import { getWindow } from '../../../base/browser/dom.js';
|
||||
import { IObservable, autorun, observableValue } from '../../../base/common/observable.js';
|
||||
|
||||
export class QuickInputService extends Themable implements IQuickInputService {
|
||||
|
||||
@@ -26,6 +27,9 @@ export class QuickInputService extends Themable implements IQuickInputService {
|
||||
|
||||
get backButton(): IQuickInputButton { return this.controller.backButton; }
|
||||
|
||||
private readonly _alignment = observableValue<QuickInputAlignment>(this, 'top');
|
||||
readonly alignment: IObservable<QuickInputAlignment> = this._alignment;
|
||||
|
||||
private readonly _onShow = this._register(new Emitter<void>());
|
||||
readonly onShow = this._onShow.event;
|
||||
|
||||
@@ -118,6 +122,11 @@ export class QuickInputService extends Themable implements IQuickInputService {
|
||||
this._onHide.fire();
|
||||
}));
|
||||
|
||||
// Mirror alignment from controller
|
||||
this._register(autorun(reader => {
|
||||
this._alignment.set(controller.alignment.read(reader), undefined);
|
||||
}));
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { IItemAccessor } from '../../../base/common/fuzzyScorer.js';
|
||||
import { ResolvedKeybinding } from '../../../base/common/keybindings.js';
|
||||
import { IDisposable } from '../../../base/common/lifecycle.js';
|
||||
import { Schemas } from '../../../base/common/network.js';
|
||||
import { IObservable } from '../../../base/common/observable.js';
|
||||
import Severity from '../../../base/common/severity.js';
|
||||
import { URI } from '../../../base/common/uri.js';
|
||||
import { IMarkdownString } from '../../../base/common/htmlContent.js';
|
||||
@@ -934,6 +935,8 @@ export const IQuickInputService = createDecorator<IQuickInputService>('quickInpu
|
||||
|
||||
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
||||
|
||||
export type QuickInputAlignment = 'top' | 'center' | 'custom';
|
||||
|
||||
export interface IQuickInputService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
@@ -958,6 +961,11 @@ export interface IQuickInputService {
|
||||
*/
|
||||
readonly onHide: Event<void>;
|
||||
|
||||
/**
|
||||
* The current alignment of the quick input widget.
|
||||
*/
|
||||
readonly alignment: IObservable<QuickInputAlignment>;
|
||||
|
||||
/**
|
||||
* Opens the quick input box for selecting items and returns a promise
|
||||
* with the user selected item(s) if any.
|
||||
|
||||
@@ -12,6 +12,7 @@ import { IAction, SubmenuAction } from '../../../../base/common/actions.js';
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { DisposableStore } from '../../../../base/common/lifecycle.js';
|
||||
import { autorun } from '../../../../base/common/observable.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { createActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
|
||||
import { HiddenItemStrategy, MenuWorkbenchToolBar, WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';
|
||||
@@ -61,8 +62,21 @@ export class CommandCenterControl {
|
||||
}
|
||||
});
|
||||
|
||||
this._disposables.add(Event.filter(quickInputService.onShow, () => isActiveDocument(this.element), this._disposables)(this._setVisibility.bind(this, false)));
|
||||
this._disposables.add(quickInputService.onHide(this._setVisibility.bind(this, true)));
|
||||
let quickInputVisible = false;
|
||||
this._disposables.add(Event.filter(quickInputService.onShow, () => isActiveDocument(this.element), this._disposables)(() => {
|
||||
quickInputVisible = true;
|
||||
this._setVisibility(quickInputService.alignment.get() !== 'top');
|
||||
}));
|
||||
this._disposables.add(quickInputService.onHide(() => {
|
||||
quickInputVisible = false;
|
||||
this._setVisibility(true);
|
||||
}));
|
||||
this._disposables.add(autorun(reader => {
|
||||
const alignment = quickInputService.alignment.read(reader);
|
||||
if (quickInputVisible) {
|
||||
this._setVisibility(alignment !== 'top');
|
||||
}
|
||||
}));
|
||||
this._disposables.add(titleToolbar);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { isValidBasename } from '../../../base/common/extpath.js';
|
||||
import { IMarkdownString } from '../../../base/common/htmlContent.js';
|
||||
import { Disposable, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';
|
||||
import { Schemas } from '../../../base/common/network.js';
|
||||
import { observableValue } from '../../../base/common/observable.js';
|
||||
import { posix, win32 } from '../../../base/common/path.js';
|
||||
import { IProcessEnvironment, isWindows, OperatingSystem } from '../../../base/common/platform.js';
|
||||
import { env } from '../../../base/common/process.js';
|
||||
@@ -90,7 +91,7 @@ import { TestNotificationService } from '../../../platform/notification/test/com
|
||||
import product from '../../../platform/product/common/product.js';
|
||||
import { IProductService } from '../../../platform/product/common/productService.js';
|
||||
import { IProgress, IProgressCompositeOptions, IProgressDialogOptions, IProgressIndicator, IProgressNotificationOptions, IProgressOptions, IProgressService, IProgressStep, IProgressWindowOptions, Progress } from '../../../platform/progress/common/progress.js';
|
||||
import { IInputBox, IInputOptions, IPickOptions, IQuickInputButton, IQuickInputService, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, IQuickTree, IQuickTreeItem, IQuickWidget, QuickPickInput } from '../../../platform/quickinput/common/quickInput.js';
|
||||
import { IInputBox, IInputOptions, IPickOptions, IQuickInputButton, IQuickInputService, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, IQuickTree, IQuickTreeItem, IQuickWidget, QuickInputAlignment, QuickPickInput } from '../../../platform/quickinput/common/quickInput.js';
|
||||
import { Registry } from '../../../platform/registry/common/platform.js';
|
||||
import { IRemoteAgentEnvironment } from '../../../platform/remote/common/remoteAgentEnvironment.js';
|
||||
import { IRemoteExtensionsScannerService } from '../../../platform/remote/common/remoteExtensionsScanner.js';
|
||||
@@ -1922,6 +1923,7 @@ export class TestQuickInputService implements IQuickInputService {
|
||||
readonly onShow = Event.None;
|
||||
readonly onHide = Event.None;
|
||||
|
||||
readonly alignment = observableValue('TestQuickInputService.alignment', 'top' as QuickInputAlignment);
|
||||
readonly currentQuickInput = undefined;
|
||||
readonly quickAccess = undefined!;
|
||||
backButton!: IQuickInputButton;
|
||||
|
||||
Reference in New Issue
Block a user