mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-08 20:57:08 +08:00
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:
@@ -1,4 +1,5 @@
|
|||||||
import { EdgelessTextBlockComponent } from '@blocksuite/affine-block-edgeless-text';
|
import { EdgelessTextBlockComponent } from '@blocksuite/affine-block-edgeless-text';
|
||||||
|
import { toast } from '@blocksuite/affine-components/toast';
|
||||||
import {
|
import {
|
||||||
ConnectorElementModel,
|
ConnectorElementModel,
|
||||||
ConnectorMode,
|
ConnectorMode,
|
||||||
@@ -22,7 +23,7 @@ import {
|
|||||||
isGfxGroupCompatibleModel,
|
isGfxGroupCompatibleModel,
|
||||||
} from '@blocksuite/block-std/gfx';
|
} from '@blocksuite/block-std/gfx';
|
||||||
import { IS_MAC } from '@blocksuite/global/env';
|
import { IS_MAC } from '@blocksuite/global/env';
|
||||||
import { Bound } from '@blocksuite/global/utils';
|
import { Bound, getCommonBound } from '@blocksuite/global/utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getNearestTranslation,
|
getNearestTranslation,
|
||||||
@@ -48,6 +49,10 @@ import {
|
|||||||
} from './utils/text.js';
|
} from './utils/text.js';
|
||||||
|
|
||||||
export class EdgelessPageKeyboardManager extends PageKeyboardManager {
|
export class EdgelessPageKeyboardManager extends PageKeyboardManager {
|
||||||
|
get gfx() {
|
||||||
|
return this.rootComponent.gfx;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(override rootComponent: EdgelessRootBlockComponent) {
|
constructor(override rootComponent: EdgelessRootBlockComponent) {
|
||||||
super(rootComponent);
|
super(rootComponent);
|
||||||
this.rootComponent.bindHotKey(
|
this.rootComponent.bindHotKey(
|
||||||
@@ -254,18 +259,40 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
|
|||||||
editing: false,
|
editing: false,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
'Mod-1': ctx => {
|
|
||||||
ctx.get('defaultState').event.preventDefault();
|
|
||||||
this.rootComponent.service.setZoomByAction('fit');
|
|
||||||
},
|
|
||||||
'Mod--': ctx => {
|
'Mod--': ctx => {
|
||||||
ctx.get('defaultState').event.preventDefault();
|
ctx.get('defaultState').event.preventDefault();
|
||||||
this.rootComponent.service.setZoomByAction('out');
|
this.rootComponent.service.setZoomByAction('out');
|
||||||
},
|
},
|
||||||
'Mod-0': ctx => {
|
'Alt-0': ctx => {
|
||||||
ctx.get('defaultState').event.preventDefault();
|
ctx.get('defaultState').event.preventDefault();
|
||||||
this.rootComponent.service.setZoomByAction('reset');
|
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 => {
|
'Mod-=': ctx => {
|
||||||
ctx.get('defaultState').event.preventDefault();
|
ctx.get('defaultState').event.preventDefault();
|
||||||
this.rootComponent.service.setZoomByAction('in');
|
this.rootComponent.service.setZoomByAction('in');
|
||||||
|
|||||||
@@ -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(
|
setViewportByBound(
|
||||||
bound: Bound,
|
bound: Bound,
|
||||||
padding: [number, number, number, number] = [0, 0, 0, 0],
|
padding: [number, number, number, number] = [0, 0, 0, 0],
|
||||||
smooth = false
|
smooth = false
|
||||||
) {
|
) {
|
||||||
const [pt, pr, pb, pl] = padding;
|
let [pt, pr, pb, pl] = padding;
|
||||||
const zoom = clamp(
|
|
||||||
|
// 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.width - (pr + pl)) / bound.w,
|
||||||
this.ZOOM_MIN,
|
|
||||||
(this.height - (pt + pb)) / bound.h
|
(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 = [
|
const center = [
|
||||||
bound.x + (bound.w + pr / zoom) / 2 - pl / zoom / 2,
|
bound.x + (bound.w + pr / zoom) / 2 - pl / zoom / 2,
|
||||||
bound.y + (bound.h + pb / zoom) / 2 - pt / zoom / 2,
|
bound.y + (bound.h + pb / zoom) / 2 - pt / zoom / 2,
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ import {
|
|||||||
zoomInByKeyboard,
|
zoomInByKeyboard,
|
||||||
zoomOutByKeyboard,
|
zoomOutByKeyboard,
|
||||||
zoomResetByKeyboard,
|
zoomResetByKeyboard,
|
||||||
|
zoomToSelection,
|
||||||
} from '../utils/actions/edgeless.js';
|
} from '../utils/actions/edgeless.js';
|
||||||
import {
|
import {
|
||||||
clickView,
|
clickView,
|
||||||
|
dragBetweenCoords,
|
||||||
enterPlaygroundRoom,
|
enterPlaygroundRoom,
|
||||||
focusRichText,
|
focusRichText,
|
||||||
initEmptyEdgelessState,
|
initEmptyEdgelessState,
|
||||||
@@ -236,6 +238,45 @@ test.describe('zooming', () => {
|
|||||||
zoom = await getZoomLevel(page);
|
zoom = await getZoomLevel(page);
|
||||||
expect(zoom).toBe(150);
|
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 }) => {
|
test('cmd + A should select all elements by default', async ({ page }) => {
|
||||||
|
|||||||
@@ -928,7 +928,7 @@ export async function multiTouchUp(page: Page, points: Point[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function zoomFitByKeyboard(page: Page) {
|
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);
|
await waitNextFrame(page, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -938,7 +938,13 @@ export async function zoomOutByKeyboard(page: Page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function zoomResetByKeyboard(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
|
// Wait for animation
|
||||||
await waitNextFrame(page, 300);
|
await waitNextFrame(page, 300);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type KeyboardShortcutsI18NKeys =
|
|||||||
| 'zoomOut'
|
| 'zoomOut'
|
||||||
| 'zoomTo100'
|
| 'zoomTo100'
|
||||||
| 'zoomToFit'
|
| 'zoomToFit'
|
||||||
|
| 'zoomToSelection'
|
||||||
| 'select'
|
| 'select'
|
||||||
| 'text'
|
| 'text'
|
||||||
| 'shape'
|
| 'shape'
|
||||||
@@ -114,8 +115,9 @@ export const useMacEdgelessKeyboardShortcuts = (): ShortcutMap => {
|
|||||||
[t('redo')]: ['⌘', '⇧', 'Z'],
|
[t('redo')]: ['⌘', '⇧', 'Z'],
|
||||||
[t('zoomIn')]: ['⌘', '+'],
|
[t('zoomIn')]: ['⌘', '+'],
|
||||||
[t('zoomOut')]: ['⌘', '-'],
|
[t('zoomOut')]: ['⌘', '-'],
|
||||||
[t('zoomTo100')]: ['⌘', '0'],
|
[t('zoomTo100')]: ['Alt', '0'],
|
||||||
[t('zoomToFit')]: ['⌘', '1'],
|
[t('zoomToFit')]: ['Alt', '1'],
|
||||||
|
[t('zoomToSelection')]: ['Alt', '2'],
|
||||||
[t('select')]: ['V'],
|
[t('select')]: ['V'],
|
||||||
[t('text')]: ['T'],
|
[t('text')]: ['T'],
|
||||||
[t('shape')]: ['S'],
|
[t('shape')]: ['S'],
|
||||||
@@ -140,8 +142,9 @@ export const useWinEdgelessKeyboardShortcuts = (): ShortcutMap => {
|
|||||||
[t('redo')]: ['Ctrl', 'Y/Ctrl', 'Shift', 'Z'],
|
[t('redo')]: ['Ctrl', 'Y/Ctrl', 'Shift', 'Z'],
|
||||||
[t('zoomIn')]: ['Ctrl', '+'],
|
[t('zoomIn')]: ['Ctrl', '+'],
|
||||||
[t('zoomOut')]: ['Ctrl', '-'],
|
[t('zoomOut')]: ['Ctrl', '-'],
|
||||||
[t('zoomTo100')]: ['Ctrl', '0'],
|
[t('zoomTo100')]: ['Alt', '0'],
|
||||||
[t('zoomToFit')]: ['Ctrl', '1'],
|
[t('zoomToFit')]: ['Alt', '1'],
|
||||||
|
[t('zoomToSelection')]: ['Alt', '2'],
|
||||||
[t('select')]: ['V'],
|
[t('select')]: ['V'],
|
||||||
[t('text')]: ['T'],
|
[t('text')]: ['T'],
|
||||||
[t('shape')]: ['S'],
|
[t('shape')]: ['S'],
|
||||||
|
|||||||
@@ -618,6 +618,7 @@
|
|||||||
"com.affine.keyboardShortcuts.zoomOut": "Zoom out",
|
"com.affine.keyboardShortcuts.zoomOut": "Zoom out",
|
||||||
"com.affine.keyboardShortcuts.zoomTo100": "Zoom to 100%",
|
"com.affine.keyboardShortcuts.zoomTo100": "Zoom to 100%",
|
||||||
"com.affine.keyboardShortcuts.zoomToFit": "Zoom to fit",
|
"com.affine.keyboardShortcuts.zoomToFit": "Zoom to fit",
|
||||||
|
"com.affine.keyboardShortcuts.zoomToSelection": "Zoom to selection",
|
||||||
"com.affine.last30Days": "Last 30 days",
|
"com.affine.last30Days": "Last 30 days",
|
||||||
"com.affine.last7Days": "Last 7 days",
|
"com.affine.last7Days": "Last 7 days",
|
||||||
"com.affine.lastMonth": "Last month",
|
"com.affine.lastMonth": "Last month",
|
||||||
|
|||||||
@@ -600,6 +600,7 @@
|
|||||||
"com.affine.keyboardShortcuts.zoomOut": "缩小",
|
"com.affine.keyboardShortcuts.zoomOut": "缩小",
|
||||||
"com.affine.keyboardShortcuts.zoomTo100": "缩放至 100%",
|
"com.affine.keyboardShortcuts.zoomTo100": "缩放至 100%",
|
||||||
"com.affine.keyboardShortcuts.zoomToFit": "自适应缩放",
|
"com.affine.keyboardShortcuts.zoomToFit": "自适应缩放",
|
||||||
|
"com.affine.keyboardShortcuts.zoomToSelection": "缩放至选中元素",
|
||||||
"com.affine.last30Days": "过去 30 天",
|
"com.affine.last30Days": "过去 30 天",
|
||||||
"com.affine.last7Days": "过去 7 天",
|
"com.affine.last7Days": "过去 7 天",
|
||||||
"com.affine.lastMonth": "上个月",
|
"com.affine.lastMonth": "上个月",
|
||||||
|
|||||||
@@ -597,6 +597,7 @@
|
|||||||
"com.affine.keyboardShortcuts.zoomOut": "縮小",
|
"com.affine.keyboardShortcuts.zoomOut": "縮小",
|
||||||
"com.affine.keyboardShortcuts.zoomTo100": "縮放至 100%",
|
"com.affine.keyboardShortcuts.zoomTo100": "縮放至 100%",
|
||||||
"com.affine.keyboardShortcuts.zoomToFit": "縮放至適當尺寸",
|
"com.affine.keyboardShortcuts.zoomToFit": "縮放至適當尺寸",
|
||||||
|
"com.affine.keyboardShortcuts.zoomToSelection": "縮放至選擇元素",
|
||||||
"com.affine.last30Days": "最近 30 天",
|
"com.affine.last30Days": "最近 30 天",
|
||||||
"com.affine.last7Days": "最近 7 天",
|
"com.affine.last7Days": "最近 7 天",
|
||||||
"com.affine.lastMonth": "上個月",
|
"com.affine.lastMonth": "上個月",
|
||||||
|
|||||||
Reference in New Issue
Block a user