feat: add shortcut of zooming to selection (#9447)

### Changed
- change edgeless shortcut `cmd + 0`, `cmd + 1` to `alt + 0`, `alt + 1`
- add new shortcut `alt + 2` to zoom to currently selected elements
This commit is contained in:
doouding
2025-01-03 03:57:04 +00:00
parent 30a181da38
commit a4e94ab72f
8 changed files with 125 additions and 15 deletions

View File

@@ -1,4 +1,5 @@
import { EdgelessTextBlockComponent } from '@blocksuite/affine-block-edgeless-text';
import { toast } from '@blocksuite/affine-components/toast';
import {
ConnectorElementModel,
ConnectorMode,
@@ -22,7 +23,7 @@ import {
isGfxGroupCompatibleModel,
} from '@blocksuite/block-std/gfx';
import { IS_MAC } from '@blocksuite/global/env';
import { Bound } from '@blocksuite/global/utils';
import { Bound, getCommonBound } from '@blocksuite/global/utils';
import {
getNearestTranslation,
@@ -48,6 +49,10 @@ import {
} from './utils/text.js';
export class EdgelessPageKeyboardManager extends PageKeyboardManager {
get gfx() {
return this.rootComponent.gfx;
}
constructor(override rootComponent: EdgelessRootBlockComponent) {
super(rootComponent);
this.rootComponent.bindHotKey(
@@ -254,18 +259,40 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
editing: false,
});
},
'Mod-1': ctx => {
ctx.get('defaultState').event.preventDefault();
this.rootComponent.service.setZoomByAction('fit');
},
'Mod--': ctx => {
ctx.get('defaultState').event.preventDefault();
this.rootComponent.service.setZoomByAction('out');
},
'Mod-0': ctx => {
'Alt-0': ctx => {
ctx.get('defaultState').event.preventDefault();
this.rootComponent.service.setZoomByAction('reset');
},
'Alt-1': ctx => {
ctx.get('defaultState').event.preventDefault();
this.rootComponent.service.setZoomByAction('fit');
},
'Alt-2': ctx => {
ctx.get('defaultState').event.preventDefault();
const selectedElements = this.gfx.selection.selectedElements;
if (selectedElements.length === 0) {
return;
}
const bound = getCommonBound(selectedElements);
if (bound === null) {
return;
}
toast(this.rootComponent.host, 'Zoom to selection');
this.gfx.viewport.setViewportByBound(
bound,
[0.12, 0.12, 0.12, 0.12],
true
);
},
'Mod-=': ctx => {
ctx.get('defaultState').event.preventDefault();
this.rootComponent.service.setZoomByAction('in');

View File

@@ -278,17 +278,47 @@ export class Viewport {
}
}
/**
* Set the viewport to fit the bound with padding.
* @param bound The bound will be zoomed to fit the viewport.
* @param padding The padding will be applied to the bound after zooming, default is [0, 0, 0, 0],
* the value may be reduced if there is not enough space for the padding.
* Use decimal less than 1 to represent percentage padding. e.g. [0.1, 0.1, 0.1, 0.1] means 10% padding.
* @param smooth whether to animate the zooming
*/
setViewportByBound(
bound: Bound,
padding: [number, number, number, number] = [0, 0, 0, 0],
smooth = false
) {
const [pt, pr, pb, pl] = padding;
const zoom = clamp(
let [pt, pr, pb, pl] = padding;
// Convert percentage padding to absolute values if they are between 0 and 1
if (pt > 0 && pt < 1) pt *= this.height;
if (pr > 0 && pr < 1) pr *= this.width;
if (pb > 0 && pb < 1) pb *= this.height;
if (pl > 0 && pl < 1) pl *= this.width;
// Calculate zoom
let zoom = Math.min(
(this.width - (pr + pl)) / bound.w,
this.ZOOM_MIN,
(this.height - (pt + pb)) / bound.h
);
// Adjust padding if space is not enough
if (zoom < this.ZOOM_MIN) {
zoom = this.ZOOM_MIN;
const totalPaddingWidth = this.width - bound.w * zoom;
const totalPaddingHeight = this.height - bound.h * zoom;
pr = pl = Math.max(totalPaddingWidth / 2, 1);
pt = pb = Math.max(totalPaddingHeight / 2, 1);
}
// Ensure zoom does not exceed ZOOM_MAX
if (zoom > this.ZOOM_MAX) {
zoom = this.ZOOM_MAX;
}
const center = [
bound.x + (bound.w + pr / zoom) / 2 - pl / zoom / 2,
bound.y + (bound.h + pb / zoom) / 2 - pt / zoom / 2,

View File

@@ -15,9 +15,11 @@ import {
zoomInByKeyboard,
zoomOutByKeyboard,
zoomResetByKeyboard,
zoomToSelection,
} from '../utils/actions/edgeless.js';
import {
clickView,
dragBetweenCoords,
enterPlaygroundRoom,
focusRichText,
initEmptyEdgelessState,
@@ -236,6 +238,45 @@ test.describe('zooming', () => {
zoom = await getZoomLevel(page);
expect(zoom).toBe(150);
});
test('zoom to selection', async ({ page }) => {
await enterPlaygroundRoom(page);
await initEmptyEdgelessState(page);
await switchEditorMode(page);
await zoomToSelection(page);
const start = { x: 0, y: 0 };
const end = { x: 900, y: 200 };
await addBasicRectShapeElement(page, start, end);
await page.keyboard.down('Space');
await dragBetweenCoords(
page,
{
x: 200,
y: 200,
},
{
x: 200 - 50,
y: 200 - 50,
}
);
await page.keyboard.up('Space');
await zoomFitByKeyboard(page);
const shapeContained = await page.evaluate(() => {
const edgelessBlock = document.querySelector('affine-edgeless-root');
if (!edgelessBlock) {
throw new Error('edgeless block not found');
}
const gfx = edgelessBlock.gfx;
const element = gfx.selection.selectedElements[0];
return gfx.viewport.viewportBounds.contains(element.elementBound);
});
expect(shapeContained).toBe(true);
});
});
test('cmd + A should select all elements by default', async ({ page }) => {

View File

@@ -928,7 +928,7 @@ export async function multiTouchUp(page: Page, points: Point[]) {
}
export async function zoomFitByKeyboard(page: Page) {
await page.keyboard.press(`${SHORT_KEY}+1`, { delay: 100 });
await page.keyboard.press(`Alt+1`, { delay: 100 });
await waitNextFrame(page, 300);
}
@@ -938,7 +938,13 @@ export async function zoomOutByKeyboard(page: Page) {
}
export async function zoomResetByKeyboard(page: Page) {
await page.keyboard.press(`${SHORT_KEY}+0`, { delay: 50 });
await page.keyboard.press(`Alt+0`, { delay: 50 });
// Wait for animation
await waitNextFrame(page, 300);
}
export async function zoomToSelection(page: Page) {
await page.keyboard.press(`Alt+2`, { delay: 50 });
// Wait for animation
await waitNextFrame(page, 300);
}