From cb5d7eaabc2b72ae393151ae53cf58ff7852e0ab Mon Sep 17 00:00:00 2001 From: doouding Date: Fri, 3 Jan 2025 06:09:10 +0000 Subject: [PATCH] feat: add scroll wheel zoom setting (#9476) ### Changed Add `scroll wheel to zoom` setting option, when the option enables, user can zoom in and out with scroll wheel without pressing the cmd/ctrl key. --- .../src/services/editor-setting-service.ts | 10 ++- .../edgeless/edgeless-root-block.ts | 5 +- .../playground/apps/_common/mock-services.ts | 29 +++++++- .../playground/apps/default/utils/editor.ts | 3 + blocksuite/playground/apps/starter/main.ts | 6 +- .../playground/apps/starter/utils/editor.ts | 3 + .../tests-legacy/edgeless/basic.spec.ts | 44 ++++++++++++ .../tests-legacy/utils/actions/edgeless.ts | 9 ++- .../editor/edgeless/general.tsx | 70 ++++++++++++------- .../core/src/modules/editor-setting/schema.ts | 6 +- packages/frontend/i18n/src/resources/en.json | 2 + .../frontend/i18n/src/resources/zh-Hans.json | 2 + .../frontend/i18n/src/resources/zh-Hant.json | 2 + 13 files changed, 155 insertions(+), 36 deletions(-) diff --git a/blocksuite/affine/shared/src/services/editor-setting-service.ts b/blocksuite/affine/shared/src/services/editor-setting-service.ts index 854e634941..aad75b750b 100644 --- a/blocksuite/affine/shared/src/services/editor-setting-service.ts +++ b/blocksuite/affine/shared/src/services/editor-setting-service.ts @@ -2,13 +2,17 @@ import type { ExtensionType } from '@blocksuite/block-std'; import { createIdentifier } from '@blocksuite/global/di'; import type { DeepPartial } from '@blocksuite/global/utils'; import type { Signal } from '@preact/signals-core'; -import type { z } from 'zod'; +import { z } from 'zod'; import { NodePropsSchema } from '../utils/index.js'; -export const EditorSettingSchema = NodePropsSchema; +export const GeneralSettingSchema = z + .object({ + edgelessScrollZoom: z.boolean().default(false), + }) + .merge(NodePropsSchema); -export type EditorSetting = z.infer; +export type EditorSetting = z.infer; export const EditorSettingProvider = createIdentifier< Signal> diff --git a/blocksuite/blocks/src/root-block/edgeless/edgeless-root-block.ts b/blocksuite/blocks/src/root-block/edgeless/edgeless-root-block.ts index 8f42f52abe..a6528f6137 100644 --- a/blocksuite/blocks/src/root-block/edgeless/edgeless-root-block.ts +++ b/blocksuite/blocks/src/root-block/edgeless/edgeless-root-block.ts @@ -11,6 +11,7 @@ import type { ShapeElementModel, } from '@blocksuite/affine-model'; import { + EditorSettingProvider, EditPropsStore, FontLoaderService, ThemeProvider, @@ -371,8 +372,10 @@ export class EdgelessRootBlockComponent extends BlockComponent< private _initWheelEvent() { this._disposables.add( this.dispatcher.add('wheel', ctx => { + const config = this.std.getOptional(EditorSettingProvider); const state = ctx.get('defaultState'); const e = state.event as WheelEvent; + const edgelessScrollZoom = config?.peek().edgelessScrollZoom ?? false; e.preventDefault(); @@ -380,7 +383,7 @@ export class EdgelessRootBlockComponent extends BlockComponent< if (viewport.locked) return; // zoom - if (isTouchPadPinchEvent(e)) { + if (isTouchPadPinchEvent(e) || edgelessScrollZoom) { const rect = this.getBoundingClientRect(); // Perform zooming relative to the mouse position const [baseX, baseY] = this.gfx.viewport.toModelCoord( diff --git a/blocksuite/playground/apps/_common/mock-services.ts b/blocksuite/playground/apps/_common/mock-services.ts index a9d21aa7ad..a3566c5521 100644 --- a/blocksuite/playground/apps/_common/mock-services.ts +++ b/blocksuite/playground/apps/_common/mock-services.ts @@ -3,11 +3,13 @@ import type { PeekViewService, } from '@blocksuite/affine-components/peek'; import { PeekViewExtension } from '@blocksuite/affine-components/peek'; +import type { EditorSetting } from '@blocksuite/affine-shared/services'; import { BlockComponent } from '@blocksuite/block-std'; import { ColorScheme, type DocMode, type DocModeProvider, + GeneralSettingSchema, type GenerateDocUrlService, matchFlavours, type NotificationService, @@ -19,7 +21,7 @@ import { import { Slot } from '@blocksuite/global/utils'; import type { AffineEditorContainer } from '@blocksuite/presets'; import { type DocCollection } from '@blocksuite/store'; -import { signal } from '@preact/signals-core'; +import { Signal, signal } from '@preact/signals-core'; import type { TemplateResult } from 'lit'; import type { AttachmentViewerPanel } from './components/attachment-viewer-panel.js'; @@ -204,3 +206,28 @@ export function mockGenerateDocUrlService(collection: DocCollection) { }; return generateDocUrlService; } + +export function mockEditorSetting() { + if (window.editorSetting$) return window.editorSetting$; + + const initialVal = Object.entries(GeneralSettingSchema.shape).reduce( + (pre: EditorSetting, [key, schema]) => { + // @ts-expect-error key is EditorSetting field + pre[key as keyof EditorSetting] = schema.parse(undefined); + return pre; + }, + {} as EditorSetting + ); + + const signal = new Signal(initialVal); + + window.editorSetting$ = signal; + + return signal; +} + +declare global { + interface Window { + editorSetting$: Signal; + } +} diff --git a/blocksuite/playground/apps/default/utils/editor.ts b/blocksuite/playground/apps/default/utils/editor.ts index 5db499beae..edd1a20323 100644 --- a/blocksuite/playground/apps/default/utils/editor.ts +++ b/blocksuite/playground/apps/default/utils/editor.ts @@ -3,6 +3,7 @@ import { CommunityCanvasTextFonts, DocModeExtension, DocModeProvider, + EditorSettingExtension, FontConfigExtension, GenerateDocUrlExtension, GenerateDocUrlProvider, @@ -26,6 +27,7 @@ import { } from '../../_common/history.js'; import { mockDocModeService, + mockEditorSetting, mockGenerateDocUrlService, mockNotificationService, mockParseDocUrlService, @@ -126,6 +128,7 @@ export async function mountDefaultDocEditor(collection: DocCollection) { NotificationExtension(mockNotificationService(editor)), FontConfigExtension(CommunityCanvasTextFonts), mockPeekViewExtension(attachmentViewerPanel), + EditorSettingExtension(mockEditorSetting()), ]; return newSpec; } diff --git a/blocksuite/playground/apps/starter/main.ts b/blocksuite/playground/apps/starter/main.ts index 9b82ed1f69..865dbbc513 100644 --- a/blocksuite/playground/apps/starter/main.ts +++ b/blocksuite/playground/apps/starter/main.ts @@ -22,7 +22,10 @@ import { effects as presetsEffects } from '@blocksuite/presets/effects'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports import * as store from '@blocksuite/store'; -import { mockDocModeService } from '../_common/mock-services.js'; +import { + mockDocModeService, + mockEditorSetting, +} from '../_common/mock-services.js'; import { setupEdgelessTemplate } from '../_common/setup.js'; import { createStarterDocCollection, @@ -59,6 +62,7 @@ async function main() { }, defaultExtensions: (): ExtensionType[] => [ FontConfigExtension(CommunityCanvasTextFonts), + blocks.EditorSettingExtension(mockEditorSetting()), ], extensions: { FontConfigExtension: FontConfigExtension(CommunityCanvasTextFonts), diff --git a/blocksuite/playground/apps/starter/utils/editor.ts b/blocksuite/playground/apps/starter/utils/editor.ts index 6357aef0b5..1fe2ff681c 100644 --- a/blocksuite/playground/apps/starter/utils/editor.ts +++ b/blocksuite/playground/apps/starter/utils/editor.ts @@ -7,6 +7,7 @@ import { AffineFormatBarWidget, CommunityCanvasTextFonts, DocModeProvider, + EditorSettingExtension, FontConfigExtension, GenerateDocUrlExtension, NotificationExtension, @@ -35,6 +36,7 @@ import { } from '../../_common/history.js'; import { mockDocModeService, + mockEditorSetting, mockGenerateDocUrlService, mockNotificationService, mockParseDocUrlService, @@ -79,6 +81,7 @@ export async function mountDefaultDocEditor(collection: DocCollection) { GenerateDocUrlExtension(mockGenerateDocUrlService(collection)), NotificationExtension(mockNotificationService(editor)), OverrideThemeExtension(themeExtension), + EditorSettingExtension(mockEditorSetting()), { setup: di => { di.override(DocModeProvider, () => diff --git a/blocksuite/tests-legacy/edgeless/basic.spec.ts b/blocksuite/tests-legacy/edgeless/basic.spec.ts index 392a6cb059..7c17f081bf 100644 --- a/blocksuite/tests-legacy/edgeless/basic.spec.ts +++ b/blocksuite/tests-legacy/edgeless/basic.spec.ts @@ -120,6 +120,50 @@ test('zoom by mouse', async ({ page }) => { await assertEdgelessSelectedModelRect(page, zoomed); }); +test('zoom by mouse without ctrl pressed when edgelessScrollZoom is enabled', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyEdgelessState(page); + + await switchEditorMode(page); + await zoomResetByKeyboard(page); + await assertZoomLevel(page, 100); + + await assertNoteXYWH(page, [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]); + + await page.mouse.click(CENTER_X, CENTER_Y); + const original = [0, 0, DEFAULT_NOTE_WIDTH, DEFAULT_NOTE_HEIGHT]; + await assertEdgelessSelectedModelRect(page, original); + + // enable edgelessScrollZoom + await page.evaluate(() => { + window.editorSetting$.value = { + ...window.editorSetting$.value, + edgelessScrollZoom: true, + }; + }); + + // can zoom without ctrl pressed + await zoomByMouseWheel(page, 0, 125, false); + await assertZoomLevel(page, 90); + + const zoomed = [0, 0, original[2] * 0.9, original[3] * 0.9]; + await assertEdgelessSelectedModelRect(page, zoomed); + + // disable edgelessScrollZoom + await page.evaluate(() => { + window.editorSetting$.value = { + ...window.editorSetting$.value, + edgelessScrollZoom: false, + }; + }); + + // can't zoom without ctrl pressed + await zoomByMouseWheel(page, 0, 125, false); + await assertZoomLevel(page, 90); +}); + test('zoom by pinch', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyEdgelessState(page); diff --git a/blocksuite/tests-legacy/utils/actions/edgeless.ts b/blocksuite/tests-legacy/utils/actions/edgeless.ts index c137787357..68db1cc06d 100644 --- a/blocksuite/tests-legacy/utils/actions/edgeless.ts +++ b/blocksuite/tests-legacy/utils/actions/edgeless.ts @@ -825,11 +825,14 @@ export async function clickComponentToolbarMoreMenuButton( export async function zoomByMouseWheel( page: Page, stepX: number, - stepY: number + stepY: number, + pressedKey = true ) { - await page.keyboard.down(SHORT_KEY); + if (pressedKey) await page.keyboard.down(SHORT_KEY); + await page.mouse.wheel(stepX, stepY); - await page.keyboard.up(SHORT_KEY); + + if (pressedKey) await page.keyboard.up(SHORT_KEY); } // touch screen is not supported by Playwright now diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/general.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/general.tsx index 36b392e7b6..85c7a602f4 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/general.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/edgeless/general.tsx @@ -1,10 +1,10 @@ -import { Menu, MenuItem, MenuTrigger } from '@affine/component'; +import { Menu, MenuItem, MenuTrigger, Switch } from '@affine/component'; import { SettingRow } from '@affine/component/setting-components'; import { EditorSettingService } from '@affine/core/modules/editor-setting'; import type { EdgelessDefaultTheme } from '@affine/core/modules/editor-setting/schema'; import { useI18n } from '@affine/i18n'; import { useLiveData, useService } from '@toeverything/infra'; -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { menuTrigger } from '../style.css'; @@ -78,29 +78,51 @@ export const GeneralEdgelessSetting = () => { }); }, [editorSetting, items, edgelessDefaultTheme]); + const handleScrollZoomChange = useCallback( + (checked: boolean) => { + editorSetting.set('edgelessScrollZoom', checked); + }, + [editorSetting] + ); + return ( - - + - - {currentTheme} - - - + + + {currentTheme} + + + + + + + ); }; diff --git a/packages/frontend/core/src/modules/editor-setting/schema.ts b/packages/frontend/core/src/modules/editor-setting/schema.ts index e0b16e1a1b..96b80a137d 100644 --- a/packages/frontend/core/src/modules/editor-setting/schema.ts +++ b/packages/frontend/core/src/modules/editor-setting/schema.ts @@ -1,7 +1,7 @@ -import { NodePropsSchema } from '@blocksuite/affine-shared/utils'; +import { GeneralSettingSchema } from '@blocksuite/affine-shared/services'; import { z } from 'zod'; -export const BSEditorSettingSchema = NodePropsSchema; +export const BSEditorSettingSchema = GeneralSettingSchema; export type FontFamily = 'Sans' | 'Serif' | 'Mono' | 'Custom'; export type EdgelessDefaultTheme = 'auto' | 'dark' | 'light' | 'specified'; @@ -32,5 +32,5 @@ export const EditorSettingSchema = BSEditorSettingSchema.merge( AffineEditorSettingSchema ); -// eslint-disable-next-line no-redeclare +// oxlint-disable-next-line no-redeclare export type EditorSettingSchema = z.infer; diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index c69428284a..fc3c770c9a 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1236,6 +1236,8 @@ "com.affine.settings.editorSettings.page.edgeless-default-theme.description": "Set edgeless default color scheme.", "com.affine.settings.editorSettings.page.edgeless-default-theme.title": "Edgeless default theme", "com.affine.settings.editorSettings.page.edgeless-default-theme.specified": "Specified by current color mode", + "com.affine.settings.editorSettings.page.edgeless-scroll-wheel-zoom.title": "Scroll wheel zoom", + "com.affine.settings.editorSettings.page.edgeless-scroll-wheel-zoom.description": "Use the scroll wheel to zoom in and out.", "com.affine.settings.editorSettings.preferences": "Preferences", "com.affine.settings.editorSettings.preferences.export.description": "You can export the entire preferences data for backup, and the exported data can be re-imported.", "com.affine.settings.editorSettings.preferences.export.title": "Export Settings", diff --git a/packages/frontend/i18n/src/resources/zh-Hans.json b/packages/frontend/i18n/src/resources/zh-Hans.json index cbaff94635..eb730eb9e8 100644 --- a/packages/frontend/i18n/src/resources/zh-Hans.json +++ b/packages/frontend/i18n/src/resources/zh-Hans.json @@ -1143,6 +1143,8 @@ "com.affine.settings.editorSettings.page.edgeless-default-theme.description": "设置默认的无界配色方案。", "com.affine.settings.editorSettings.page.edgeless-default-theme.title": "无界默认配色方案", "com.affine.settings.editorSettings.page.edgeless-default-theme.specified": "由当前应用的配色方案指定", + "com.affine.settings.editorSettings.page.edgeless-scroll-wheel-zoom.title": "滚轮缩放", + "com.affine.settings.editorSettings.page.edgeless-scroll-wheel-zoom.description": "使用鼠标滚轮来调整缩放比例", "com.affine.settings.editorSettings.preferences": "首选项", "com.affine.settings.editorSettings.preferences.export.description": "您可以导出整个首选项数据进行备份,然后可以重新导入导出的数据。", "com.affine.settings.editorSettings.preferences.export.title": "导出设置", diff --git a/packages/frontend/i18n/src/resources/zh-Hant.json b/packages/frontend/i18n/src/resources/zh-Hant.json index a435c4c2a0..4f047db2e2 100644 --- a/packages/frontend/i18n/src/resources/zh-Hant.json +++ b/packages/frontend/i18n/src/resources/zh-Hant.json @@ -1140,6 +1140,8 @@ "com.affine.settings.editorSettings.page.edgeless-default-theme.description": "設定無界的預設配色。", "com.affine.settings.editorSettings.page.edgeless-default-theme.title": "無界預設主題", "com.affine.settings.editorSettings.page.edgeless-default-theme.specified": "由目前顏色模式指定", + "com.affine.settings.editorSettings.page.edgeless-scroll-wheel-zoom.title": "滾輪縮放", + "com.affine.settings.editorSettings.page.edgeless-scroll-wheel-zoom.description": "使用滑鼠滾輪來調整縮放比例", "com.affine.settings.editorSettings.preferences": "首選項", "com.affine.settings.editorSettings.preferences.export.description": "您可以匯出整個首選項數據進行備份,導出的數據亦可重新匯入。", "com.affine.settings.editorSettings.preferences.export.title": "匯出設定",