fix(editor): mid button drag in presentation mode (#12309)

Fixes https://linear.app/affine-design/issue/BS-3448

Before this PR, presentation mode would force quit if user either:

1. Press space
2. Drag with mouse middle button

Unfixed behavior:

https://github.com/user-attachments/assets/8ff4e13a-69a8-4de6-8994-bf36e6e3eb49

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Bug Fixes**
	- Improved presentation mode to preserve your current panned view when exiting pan mode or toggling fullscreen, preventing unwanted viewport resets.
	- Spacebar actions are now correctly disabled when using the frame navigator tool, avoiding accidental tool switches.
- **New Features**
	- Enhanced presentation controls for smoother transitions and better handling of user navigation states.
	- Added a one-time toast notification for presentations without frames, shown only once per session for better user guidance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
doodlewind
2025-05-15 11:12:40 +00:00
parent 147fa9a6b1
commit b6e9c41ee3
5 changed files with 134 additions and 34 deletions

View File

@@ -185,9 +185,33 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
if (document.fullscreenElement) {
document.exitFullscreen().catch(console.error);
}
// Reset the flag when fully exiting presentation mode
this.edgeless.std
.get(EditPropsStore)
.setStorage('presentNoFrameToastShown', false);
}
private _moveToCurrentFrame() {
private _moveToCurrentFrame(forceMove = false) {
const currentToolOption = this.gfx.tool.currentToolOption$.value;
const toolOptions = currentToolOption?.options;
// If PresentTool is being activated after a temporary pan (indicated by restoredAfterPan)
// and a forced move isn't explicitly requested, skip moving to the current frame.
// This preserves the user's panned position instead of resetting to the frame's default view.
if (
currentToolOption?.toolType === PresentTool &&
toolOptions?.restoredAfterPan &&
!forceMove
) {
// Clear the flag so future navigations behave normally
this.gfx.tool.setTool(PresentTool, {
...toolOptions,
restoredAfterPan: false,
});
return;
}
const current = this._currentFrameIndex;
const viewport = this.gfx.viewport;
const frame = this._frames[current];
@@ -263,28 +287,56 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
_disposables.add(
effect(() => {
const currentTool = this.gfx.tool.currentToolOption$.value;
const selection = this.gfx.selection;
const currentToolOption = this.gfx.tool.currentToolOption$.value;
if (currentTool?.toolType === PresentTool) {
this._cachedIndex = this._currentFrameIndex;
this._navigatorMode =
(currentTool.options as ToolOptions<PresentTool>)?.mode ??
this._navigatorMode;
if (isFrameBlock(selection.selectedElements[0])) {
this._cachedIndex = this._frames.findIndex(
frame => frame.id === selection.selectedElements[0].id
);
if (currentToolOption?.toolType === PresentTool) {
const opts = currentToolOption.options as
| ToolOptions<PresentTool>
| undefined;
const isAlreadyFullscreen = !!document.fullscreenElement;
if (!isAlreadyFullscreen) {
this._toggleFullScreen();
} else {
this._fullScreenMode = true;
}
if (this._frames.length === 0)
toast(
this.host,
'The presentation requires at least 1 frame. You can firstly create a frame.',
5000
);
this._toggleFullScreen();
}
this._cachedIndex = this._currentFrameIndex;
this._navigatorMode = opts?.mode ?? this._navigatorMode;
const selection = this.gfx.selection;
if (
selection.selectedElements.length > 0 &&
isFrameBlock(selection.selectedElements[0])
) {
const selectedFrameId = selection.selectedElements[0].id;
const indexOfSelectedFrame = this._frames.findIndex(
frame => frame.id === selectedFrameId
);
if (indexOfSelectedFrame !== -1) {
this._cachedIndex = indexOfSelectedFrame;
}
}
const store = this.edgeless.std.get(EditPropsStore);
if (this._frames.length === 0) {
if (!store.getStorage('presentNoFrameToastShown')) {
toast(
this.host,
'The presentation requires at least 1 frame. You can firstly create a frame.',
5000
);
store.setStorage('presentNoFrameToastShown', true);
}
} else {
// If frames exist, and the flag was set, reset it.
// This allows the toast to show again if all frames are subsequently deleted.
if (store.getStorage('presentNoFrameToastShown')) {
store.setStorage('presentNoFrameToastShown', false);
}
}
}
this.requestUpdate();
})
);
@@ -305,12 +357,10 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
_disposables.addFromEvent(document, 'fullscreenchange', () => {
if (document.fullscreenElement) {
// When enter fullscreen, we need to set current frame to the cached index
this._timer = setTimeout(() => {
this._currentFrameIndex = this._cachedIndex;
}, 400);
} else {
// When exit fullscreen, we need to clear the timer
clearTimeout(this._timer);
if (
this.edgelessTool.toolType === PresentTool &&
@@ -324,7 +374,7 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
}
}
setTimeout(() => this._moveToCurrentFrame(), 400);
setTimeout(() => this._moveToCurrentFrame(true), 400);
this.slots.fullScreenToggled.next();
});
@@ -430,11 +480,29 @@ export class PresentationToolbar extends EdgelessToolbarToolMixin(
}
protected override updated(changedProperties: PropertyValues) {
if (
changedProperties.has('_currentFrameIndex') &&
this.edgelessTool.toolType === PresentTool
) {
this._moveToCurrentFrame();
const currentToolOption = this.gfx.tool.currentToolOption$.value;
const isPresentToolActive = currentToolOption?.toolType === PresentTool;
const toolOptions = currentToolOption?.options;
const isRestoredAfterPan = !!(
isPresentToolActive && toolOptions?.restoredAfterPan
);
if (changedProperties.has('_currentFrameIndex') && isPresentToolActive) {
// When the current frame index changes (e.g., user navigates), a viewport update is needed.
// However, if PresentTool is merely being restored after a pan (isRestoredAfterPan = true)
// without an explicit index change in this update cycle, we avoid forcing a move to preserve the panned position.
// Thus, `forceMove` is true unless it's a pan restoration.
const shouldForceMove = !isRestoredAfterPan;
this._moveToCurrentFrame(shouldForceMove);
} else if (isPresentToolActive && changedProperties.has('edgelessTool')) {
// Handles cases where the tool is set/switched to PresentTool (e.g., initial activation or returning from another tool).
// Similar to frame index changes, avoid forcing a viewport move if restoring after a pan.
const currentToolIsPresentTool =
this.edgelessTool.toolType === PresentTool;
if (currentToolIsPresentTool) {
const shouldForceMoveOnToolChange = !isRestoredAfterPan;
this._moveToCurrentFrame(shouldForceMoveOnToolChange);
}
}
}

View File

@@ -4,6 +4,7 @@ import type { NavigatorMode } from './frame-manager';
export type PresentToolOption = {
mode?: NavigatorMode;
restoredAfterPan?: boolean;
};
export class PresentTool extends BaseTool<PresentToolOption> {