From a4e94ab72f743fb47ca4c05ced151deffeac42b1 Mon Sep 17 00:00:00 2001 From: doouding Date: Fri, 3 Jan 2025 03:57:04 +0000 Subject: [PATCH] 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 --- .../root-block/edgeless/edgeless-keyboard.ts | 39 +++++++++++++++--- .../framework/block-std/src/gfx/viewport.ts | 36 ++++++++++++++-- .../tests-legacy/edgeless/shortcut.spec.ts | 41 +++++++++++++++++++ .../tests-legacy/utils/actions/edgeless.ts | 10 ++++- .../components/hooks/affine/use-shortcuts.ts | 11 +++-- packages/frontend/i18n/src/resources/en.json | 1 + .../frontend/i18n/src/resources/zh-Hans.json | 1 + .../frontend/i18n/src/resources/zh-Hant.json | 1 + 8 files changed, 125 insertions(+), 15 deletions(-) diff --git a/blocksuite/blocks/src/root-block/edgeless/edgeless-keyboard.ts b/blocksuite/blocks/src/root-block/edgeless/edgeless-keyboard.ts index 7127a3ec2d..5c32e34129 100644 --- a/blocksuite/blocks/src/root-block/edgeless/edgeless-keyboard.ts +++ b/blocksuite/blocks/src/root-block/edgeless/edgeless-keyboard.ts @@ -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'); diff --git a/blocksuite/framework/block-std/src/gfx/viewport.ts b/blocksuite/framework/block-std/src/gfx/viewport.ts index 6d73b8241d..485b32e799 100644 --- a/blocksuite/framework/block-std/src/gfx/viewport.ts +++ b/blocksuite/framework/block-std/src/gfx/viewport.ts @@ -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, diff --git a/blocksuite/tests-legacy/edgeless/shortcut.spec.ts b/blocksuite/tests-legacy/edgeless/shortcut.spec.ts index f77e4c9513..2ae0579b65 100644 --- a/blocksuite/tests-legacy/edgeless/shortcut.spec.ts +++ b/blocksuite/tests-legacy/edgeless/shortcut.spec.ts @@ -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 }) => { diff --git a/blocksuite/tests-legacy/utils/actions/edgeless.ts b/blocksuite/tests-legacy/utils/actions/edgeless.ts index 7f532ba9ea..c137787357 100644 --- a/blocksuite/tests-legacy/utils/actions/edgeless.ts +++ b/blocksuite/tests-legacy/utils/actions/edgeless.ts @@ -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); } diff --git a/packages/frontend/core/src/components/hooks/affine/use-shortcuts.ts b/packages/frontend/core/src/components/hooks/affine/use-shortcuts.ts index a974b26011..e83b36aa7d 100644 --- a/packages/frontend/core/src/components/hooks/affine/use-shortcuts.ts +++ b/packages/frontend/core/src/components/hooks/affine/use-shortcuts.ts @@ -16,6 +16,7 @@ type KeyboardShortcutsI18NKeys = | 'zoomOut' | 'zoomTo100' | 'zoomToFit' + | 'zoomToSelection' | 'select' | 'text' | 'shape' @@ -114,8 +115,9 @@ export const useMacEdgelessKeyboardShortcuts = (): ShortcutMap => { [t('redo')]: ['⌘', '⇧', 'Z'], [t('zoomIn')]: ['⌘', '+'], [t('zoomOut')]: ['⌘', '-'], - [t('zoomTo100')]: ['⌘', '0'], - [t('zoomToFit')]: ['⌘', '1'], + [t('zoomTo100')]: ['Alt', '0'], + [t('zoomToFit')]: ['Alt', '1'], + [t('zoomToSelection')]: ['Alt', '2'], [t('select')]: ['V'], [t('text')]: ['T'], [t('shape')]: ['S'], @@ -140,8 +142,9 @@ export const useWinEdgelessKeyboardShortcuts = (): ShortcutMap => { [t('redo')]: ['Ctrl', 'Y/Ctrl', 'Shift', 'Z'], [t('zoomIn')]: ['Ctrl', '+'], [t('zoomOut')]: ['Ctrl', '-'], - [t('zoomTo100')]: ['Ctrl', '0'], - [t('zoomToFit')]: ['Ctrl', '1'], + [t('zoomTo100')]: ['Alt', '0'], + [t('zoomToFit')]: ['Alt', '1'], + [t('zoomToSelection')]: ['Alt', '2'], [t('select')]: ['V'], [t('text')]: ['T'], [t('shape')]: ['S'], diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index de536260ab..c69428284a 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -618,6 +618,7 @@ "com.affine.keyboardShortcuts.zoomOut": "Zoom out", "com.affine.keyboardShortcuts.zoomTo100": "Zoom to 100%", "com.affine.keyboardShortcuts.zoomToFit": "Zoom to fit", + "com.affine.keyboardShortcuts.zoomToSelection": "Zoom to selection", "com.affine.last30Days": "Last 30 days", "com.affine.last7Days": "Last 7 days", "com.affine.lastMonth": "Last month", diff --git a/packages/frontend/i18n/src/resources/zh-Hans.json b/packages/frontend/i18n/src/resources/zh-Hans.json index 1b59445e50..cbaff94635 100644 --- a/packages/frontend/i18n/src/resources/zh-Hans.json +++ b/packages/frontend/i18n/src/resources/zh-Hans.json @@ -600,6 +600,7 @@ "com.affine.keyboardShortcuts.zoomOut": "缩小", "com.affine.keyboardShortcuts.zoomTo100": "缩放至 100%", "com.affine.keyboardShortcuts.zoomToFit": "自适应缩放", + "com.affine.keyboardShortcuts.zoomToSelection": "缩放至选中元素", "com.affine.last30Days": "过去 30 天", "com.affine.last7Days": "过去 7 天", "com.affine.lastMonth": "上个月", diff --git a/packages/frontend/i18n/src/resources/zh-Hant.json b/packages/frontend/i18n/src/resources/zh-Hant.json index 05946f2bf6..a435c4c2a0 100644 --- a/packages/frontend/i18n/src/resources/zh-Hant.json +++ b/packages/frontend/i18n/src/resources/zh-Hant.json @@ -597,6 +597,7 @@ "com.affine.keyboardShortcuts.zoomOut": "縮小", "com.affine.keyboardShortcuts.zoomTo100": "縮放至 100%", "com.affine.keyboardShortcuts.zoomToFit": "縮放至適當尺寸", + "com.affine.keyboardShortcuts.zoomToSelection": "縮放至選擇元素", "com.affine.last30Days": "最近 30 天", "com.affine.last7Days": "最近 7 天", "com.affine.lastMonth": "上個月",