Disable ctrl+A in notebook renderer and support Ctrl+A for inputs (#208635)

* Disable ctrl+A in notebook renderer

* Select All in input elements in outputs
This commit is contained in:
Don Jayamanne 2024-03-26 10:13:56 +11:00 committed by GitHub
parent 11f4586955
commit a8a38595ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 119 additions and 8 deletions

View file

@ -597,7 +597,7 @@ registerAction2(class extends NotebookCellAction {
title: localize('notebook.cell.output.selectAll', "Select All"),
keybinding: {
primary: KeyMod.CtrlCmd | KeyCode.KeyA,
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey), NOTEBOOK_OUTPUT_FOCUSED),
when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_OUTPUT_FOCUSED),
weight: NOTEBOOK_OUTPUT_WEBVIEW_ACTION_WEIGHT
}
});
@ -615,7 +615,11 @@ registerAction2(class extends NotebookCellAction {
if (!cell || !cell.outputIsFocused || !editor.hasWebviewFocus()) {
return true;
}
editor.selectOutputContent(cell);
if (cell.inputInOutputIsFocused) {
editor.selectInputContents(cell);
} else {
editor.selectOutputContent(cell);
}
return true;
});

View file

@ -62,6 +62,15 @@ export class DiffNestedCellViewModel extends Disposable implements IDiffNestedCe
this._onDidChangeState.fire({ outputIsFocusedChanged: true });
}
private _focusInputInOutput: boolean = false;
public get inputInOutputIsFocused(): boolean {
return this._focusInputInOutput;
}
public set inputInOutputIsFocused(v: boolean) {
this._focusInputInOutput = v;
}
private _outputViewModels: ICellOutputViewModel[];
get outputsViewModels() {

View file

@ -128,6 +128,7 @@ export interface IGenericCellViewModel {
metadata: NotebookCellMetadata;
outputIsHovered: boolean;
outputIsFocused: boolean;
inputInOutputIsFocused: boolean;
outputsViewModels: ICellOutputViewModel[];
getOutputOffset(index: number): number;
updateOutputHeight(index: number, height: number, source?: string): void;
@ -587,6 +588,11 @@ export interface INotebookEditor {
* Implementation of Ctrl+A for an output item.
*/
selectOutputContent(cell: ICellViewModel): void;
/**
* Select the active input element of the first focused output of the cell.
* Implementation of Ctrl+A for an input element in an output item.
*/
selectInputContents(cell: ICellViewModel): void;
readonly onDidReceiveMessage: Event<INotebookWebviewMessage>;

View file

@ -1961,6 +1961,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
this._webview?.selectOutputContents(cell);
}
selectInputContents(cell: ICellViewModel) {
this._webview?.selectInputContents(cell);
}
onWillHide() {
this._isVisible = false;
this._editorFocus.set(false);
@ -2402,12 +2406,14 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
return;
}
const focusElementId = options?.outputId ?? cell.id;
const firstOutputId = cell.outputsViewModels.find(o => o.model.alternativeOutputId)?.model.alternativeOutputId;
const focusElementId = options?.outputId ?? firstOutputId ?? cell.id;
this._webview.focusOutput(focusElementId, options?.altOutputId, options?.outputWebviewFocused || this._webviewFocused);
cell.updateEditState(CellEditState.Preview, 'focusNotebookCell');
cell.focusMode = CellFocusMode.Output;
cell.focusedOutputId = options?.outputId;
this._outputFocus.set(true);
if (!options?.skipReveal) {
this.revealInCenterIfOutsideViewport(cell);
}

View file

@ -686,6 +686,7 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Themable {
const latestCell = this.notebookEditor.getCellByInfo(resolvedResult.cellInfo);
if (latestCell) {
latestCell.outputIsFocused = false;
latestCell.inputInOutputIsFocused = false;
}
}
break;
@ -911,6 +912,13 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Themable {
break;
}
case 'outputInputFocus': {
const resolvedResult = this.resolveOutputId(data.id);
if (resolvedResult) {
const latestCell = this.notebookEditor.getCellByInfo(resolvedResult.cellInfo);
if (latestCell) {
latestCell.inputInOutputIsFocused = data.inputFocused;
}
}
this.notebookEditor.didFocusOutputInputChange(data.inputFocused);
}
}
@ -1693,6 +1701,18 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Themable {
});
}
selectInputContents(cell: ICellViewModel) {
if (this._disposed) {
return;
}
const output = cell.outputsViewModels.find(o => o.model.outputId === cell.focusedOutputId);
const outputId = output ? this.insetMapping.get(output)?.outputId : undefined;
this._sendMessageToWebview({
type: 'select-input-contents',
cellOrOutputId: outputId || cell.id
});
}
focusOutput(cellOrOutputId: string, alternateId: string | undefined, viewFocused: boolean) {
if (this._disposed) {
return;

View file

@ -50,6 +50,7 @@ export interface IOutputBlurMessage extends BaseToWebviewMessage {
export interface IOutputInputFocusMessage extends BaseToWebviewMessage {
readonly type: 'outputInputFocus';
readonly inputFocused: boolean;
readonly id: string;
}
export interface IScrollToRevealMessage extends BaseToWebviewMessage {
@ -463,6 +464,10 @@ export interface ISelectOutputItemMessage {
readonly type: 'select-output-contents';
readonly cellOrOutputId: string;
}
export interface ISelectInputOutputItemMessage {
readonly type: 'select-input-contents';
readonly cellOrOutputId: string;
}
export interface ILogRendererDebugMessage extends BaseToWebviewMessage {
readonly type: 'logRendererDebugMessage';
@ -544,7 +549,8 @@ export type ToWebviewMessage = IClearMessage |
IFindUnHighlightCurrentMessage |
IFindStopMessage |
IReturnOutputItemMessage |
ISelectOutputItemMessage;
ISelectOutputItemMessage |
ISelectInputOutputItemMessage;
export type AnyMessage = FromWebviewMessage | ToWebviewMessage;

View file

@ -194,11 +194,12 @@ async function webviewPreloads(ctx: PreloadContext) {
return;
}
if (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA') {
postNotebookMessage<webviewMessages.IOutputInputFocusMessage>('outputInputFocus', { inputFocused: true });
const id = lastFocusedOutput?.id;
if (id && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
postNotebookMessage<webviewMessages.IOutputInputFocusMessage>('outputInputFocus', { inputFocused: true, id });
activeElement.addEventListener('blur', () => {
postNotebookMessage<webviewMessages.IOutputInputFocusMessage>('outputInputFocus', { inputFocused: false });
postNotebookMessage<webviewMessages.IOutputInputFocusMessage>('outputInputFocus', { inputFocused: false, id });
}, { once: true });
}
};
@ -286,6 +287,17 @@ async function webviewPreloads(ctx: PreloadContext) {
};
const selectInputContents = (cellOrOutputId: string) => {
const cellOutputContainer = window.document.getElementById(cellOrOutputId);
if (!cellOutputContainer) {
return;
}
const activeElement = window.document.activeElement;
if (activeElement?.tagName === 'INPUT' || activeElement?.tagName === 'TEXTAREA') {
(activeElement as HTMLInputElement).select();
}
};
const onPageUpDownSelectionHandler = (e: KeyboardEvent) => {
if (!lastFocusedOutput?.id || !e.shiftKey) {
return;
@ -299,6 +311,11 @@ async function webviewPreloads(ctx: PreloadContext) {
if (!outputContainer || !selection?.anchorNode) {
return;
}
const activeElement = window.document.activeElement;
if (activeElement?.tagName === 'INPUT' || activeElement?.tagName === 'TEXTAREA') {
// Leave for default behavior.
return;
}
// These should change the scroll position, not adjust the selected cell in the notebook
e.stopPropagation(); // We don't want the notebook to handle this.
@ -318,6 +335,22 @@ async function webviewPreloads(ctx: PreloadContext) {
selection.addRange(range);
};
const disableNativeSelectAll = (e: KeyboardEvent) => {
if (!lastFocusedOutput?.id) {
return;
}
const activeElement = window.document.activeElement;
if (activeElement?.tagName === 'INPUT' || activeElement?.tagName === 'TEXTAREA') {
e.preventDefault(); // We will handle selection in editor code.
return;
}
if ((e.key === 'a' && e.ctrlKey) || (e.metaKey && e.key === 'a')) {
e.preventDefault(); // We will handle selection in editor code.
return;
}
};
const handleDataUrl = async (data: string | ArrayBuffer | null, downloadName: string) => {
postNotebookMessage<webviewMessages.IClickedDataUrlMessage>('clicked-data-url', {
data,
@ -343,6 +376,7 @@ async function webviewPreloads(ctx: PreloadContext) {
window.document.body.addEventListener('focusin', checkOutputInputFocus);
window.document.body.addEventListener('focusout', handleOutputFocusOut);
window.document.body.addEventListener('keydown', onPageUpDownSelectionHandler);
window.document.body.addEventListener('keydown', disableNativeSelectAll);
interface RendererContext extends rendererApi.RendererContext<unknown> {
readonly onDidChangeSettings: Event<RenderOptions>;
@ -633,13 +667,19 @@ async function webviewPreloads(ctx: PreloadContext) {
if (cellOutputContainer.contains(window.document.activeElement)) {
return;
}
const id = cellOutputContainer.id;
let focusableElement = cellOutputContainer.querySelector('[tabindex="0"], [href], button, input, option, select, textarea') as HTMLElement | null;
if (!focusableElement) {
focusableElement = cellOutputContainer;
focusableElement.tabIndex = -1;
postNotebookMessage<webviewMessages.IOutputInputFocusMessage>('outputInputFocus', { inputFocused: false, id });
} else {
const inputFocused = focusableElement.tagName === 'INPUT' || focusableElement.tagName === 'TEXTAREA';
postNotebookMessage<webviewMessages.IOutputInputFocusMessage>('outputInputFocus', { inputFocused, id });
}
lastFocusedOutput = cellOutputContainer;
postNotebookMessage<webviewMessages.IOutputFocusMessage>('outputFocus', { id: cellOutputContainer.id });
focusableElement.focus();
}
}
@ -1695,6 +1735,9 @@ async function webviewPreloads(ctx: PreloadContext) {
case 'select-output-contents':
selectOutputContents(event.data.cellOrOutputId);
break;
case 'select-input-contents':
selectInputContents(event.data.cellOrOutputId);
break;
case 'decorations': {
let outputContainer = window.document.getElementById(event.data.cellId);
if (!outputContainer) {

View file

@ -115,6 +115,15 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod
this._onDidChangeState.fire({ outputIsFocusedChanged: true });
}
private _focusInputInOutput: boolean = false;
public get inputInOutputIsFocused(): boolean {
return this._focusInputInOutput;
}
public set inputInOutputIsFocused(v: boolean) {
this._focusInputInOutput = v;
}
private _outputMinHeight: number = 0;
private get outputMinHeight() {

View file

@ -93,6 +93,14 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM
this._focusOnOutput = v;
}
public get inputInOutputIsFocused(): boolean {
return false;
}
public set inputInOutputIsFocused(_: boolean) {
//
}
private _hoveringCell = false;
public get cellIsHovered(): boolean {
return this._hoveringCell;