diff --git a/packages/sheets-ui/src/commands/commands/__tests__/set-format-painter.command.spec.ts b/packages/sheets-ui/src/commands/commands/__tests__/set-format-painter.command.spec.ts new file mode 100644 index 0000000000..748895fb09 --- /dev/null +++ b/packages/sheets-ui/src/commands/commands/__tests__/set-format-painter.command.spec.ts @@ -0,0 +1,279 @@ +/* eslint-disable no-magic-numbers */ + +import type { Univer } from '@univerjs/core'; +import { + Disposable, + ICommandService, + IUniverInstanceService, + LocaleType, + RedoCommand, + ThemeService, + UndoCommand, +} from '@univerjs/core'; +import { IRenderManagerService } from '@univerjs/engine-render'; +import { + AddWorksheetMergeMutation, + NORMAL_SELECTION_PLUGIN_NAME, + RemoveWorksheetMergeMutation, + SelectionManagerService, + SetRangeValuesCommand, + SetRangeValuesMutation, + SetSelectionsOperation, +} from '@univerjs/sheets'; +import { createCommandTestBed } from '@univerjs/sheets/commands/commands/__tests__/create-command-test-bed.js'; +import type { Injector } from '@wendellhu/redi'; +import { beforeEach, describe, expect, it } from 'vitest'; + +import { IMarkSelectionService } from '../../..'; +import { FormatPainterController } from '../../../controllers/format-painter/format-painter.controller'; +import { FormatPainterService, IFormatPainterService } from '../../../services/format-painter/format-painter.service'; +import { SetFormatPainterOperation } from '../../operations/set-format-painter.operation'; +import { + ApplyFormatPainterCommand, + SetInfiniteFormatPainterCommand, + SetOnceFormatPainterCommand, +} from '../set-format-painter.command'; + +const theme = { + colorBlack: '#35322b', +}; + +const TEST_WORKBOOK_DATA = { + id: 'workbook-01', + sheetOrder: ['sheet-0011'], + name: 'UniverSheet Demo', + appVersion: '3.0.0-alpha', + styles: { + yifA1t: { + bl: 1, + it: 1, + ul: { + s: 1, + }, + st: { + s: 1, + }, + }, + M5JbP2: { + bg: { + rgb: '#409f11', + }, + cl: { + rgb: '#E30909', + }, + ff: 'Microsoft YaHei', + }, + }, + sheets: { + 'sheet-0011': { + type: 0, + id: 'sheet-0011', + name: 'sheet11', + columnData: { + '1': { + hd: 0, + }, + }, + mergeData: [ + { + startRow: 1, + endRow: 1, + startColumn: 0, + endColumn: 1, + }, + ], + status: 1, + cellData: { + '0': { + '0': { + v: 1, + t: 2, + s: 'yifA1t', + }, + '1': { + v: 2, + t: 2, + s: 'M5JbP2', + }, + '2': { + v: 3, + }, + '3': { + v: 1, + f: '=SUM(A1)', + si: '3e4r5t', + t: 2, + }, + }, + '1': { + '0': { + v: 4, + }, + '2': { + v: 1, + t: 2, + }, + '3': { + v: 1, + t: 2, + }, + }, + '2': { + '0': { + v: 44, + }, + '2': { + v: 1, + t: 2, + }, + '3': { + v: 1, + t: 2, + }, + }, + '3': { + '0': { + v: 444, + }, + '2': { + v: 1, + t: 2, + }, + '3': { + v: 1, + t: 2, + }, + }, + '4': { + '2': { + v: 1, + t: 2, + }, + }, + }, + }, + }, + createdTime: '', + creator: '', + lastModifiedBy: '', + locale: LocaleType.EN_US, + modifiedTime: '', + timeZone: '', +}; + +class MarkSelectionService extends Disposable implements IMarkSelectionService { + addShape(): string | null { + return null; + } + + refreshShapes() {} + + removeShape(id: string): void {} + + removeAllShapes(): void {} +} + +class RenderManagerService { + getRenderById(id: string) { + return null; + } +} + +describe('Test format painter rules in controller', () => { + let univer: Univer; + let get: Injector['get']; + let commandService: ICommandService; + let themeService: ThemeService; + let formatPainterController: FormatPainterController; + beforeEach(() => { + const testBed = createCommandTestBed(TEST_WORKBOOK_DATA, [ + [IMarkSelectionService, { useClass: MarkSelectionService }], + [IFormatPainterService, { useClass: FormatPainterService }], + [IRenderManagerService, { useClass: RenderManagerService }], + [FormatPainterController], + ]); + univer = testBed.univer; + get = testBed.get; + + commandService = get(ICommandService); + themeService = get(ThemeService); + themeService.setTheme(theme); + + formatPainterController = get(FormatPainterController); + commandService.registerCommand(SetFormatPainterOperation); + commandService.registerCommand(SetInfiniteFormatPainterCommand); + commandService.registerCommand(SetOnceFormatPainterCommand); + commandService.registerCommand(ApplyFormatPainterCommand); + commandService.registerCommand(SetSelectionsOperation); + commandService.registerCommand(SetRangeValuesCommand); + commandService.registerCommand(SetRangeValuesMutation); + commandService.registerCommand(RemoveWorksheetMergeMutation); + commandService.registerCommand(AddWorksheetMergeMutation); + + const selectionManagerService = get(SelectionManagerService); + selectionManagerService.setCurrentSelection({ + pluginName: NORMAL_SELECTION_PLUGIN_NAME, + unitId: 'workbook-01', + sheetId: 'sheet-0011', + }); + }); + + describe('format painter', () => { + describe('format painter the numbers', async () => { + it('correct situation', async () => { + const workbook = get(IUniverInstanceService).getCurrentUniverSheetInstance(); + if (!workbook) throw new Error('This is an error'); + await commandService.executeCommand(SetSelectionsOperation.id, { + workbookId: 'workbook-01', + worksheetId: 'sheet-0011', + pluginName: NORMAL_SELECTION_PLUGIN_NAME, + selections: [ + { + range: { + startRow: 0, + endRow: 1, + startColumn: 0, + endColumn: 1, + }, + }, + ], + }); + await commandService.executeCommand(SetOnceFormatPainterCommand.id); + await (formatPainterController as any)._applyFormatPainter({ + startRow: 0, + endRow: 4, + startColumn: 2, + endColumn: 3, + }); + expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(0, 2)?.s).toBe('yifA1t'); + expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(0, 3)?.s).toBe('M5JbP2'); + expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(2, 3)?.s).toBe('M5JbP2'); + expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(2, 3)?.s).toBe('M5JbP2'); + expect(workbook.getSheetBySheetId('sheet-0011')?.getMergeData().length).toBe(3); + expect(workbook.getSheetBySheetId('sheet-0011')?.getMergeData()[0].startRow).toBe(1); + expect(workbook.getSheetBySheetId('sheet-0011')?.getMergeData()[1].startRow).toBe(1); + expect(workbook.getSheetBySheetId('sheet-0011')?.getMergeData()[2].startRow).toBe(3); + + get(IUniverInstanceService).focusUniverInstance('workbook-01'); + // undo + await commandService.executeCommand(UndoCommand.id); + expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(0, 2)?.s).toBe(undefined); + expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(0, 3)?.s).toBe(undefined); + expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(2, 3)?.s).toBe(undefined); + expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(2, 3)?.s).toBe(undefined); + expect(workbook.getSheetBySheetId('sheet-0011')?.getMergeData().length).toBe(1); + expect(workbook.getSheetBySheetId('sheet-0011')?.getMergeData()[0].startRow).toBe(1); + //redo + await commandService.executeCommand(RedoCommand.id); + + expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(0, 3)?.s).toBe('M5JbP2'); + expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(2, 3)?.s).toBe('M5JbP2'); + expect(workbook.getSheetBySheetId('sheet-0011')?.getCell(2, 3)?.s).toBe('M5JbP2'); + expect(workbook.getSheetBySheetId('sheet-0011')?.getMergeData().length).toBe(3); + expect(workbook.getSheetBySheetId('sheet-0011')?.getMergeData()[0].startRow).toBe(1); + expect(workbook.getSheetBySheetId('sheet-0011')?.getMergeData()[1].startRow).toBe(1); + expect(workbook.getSheetBySheetId('sheet-0011')?.getMergeData()[2].startRow).toBe(3); + }); + }); + }); +}); diff --git a/packages/sheets-ui/src/commands/commands/add-worksheet-merge.command.ts b/packages/sheets-ui/src/commands/commands/add-worksheet-merge.command.ts index e4e642d15f..59e5e3ce14 100644 --- a/packages/sheets-ui/src/commands/commands/add-worksheet-merge.command.ts +++ b/packages/sheets-ui/src/commands/commands/add-worksheet-merge.command.ts @@ -1,4 +1,4 @@ -import type { ICellData, ICommand, IMutationInfo, IRange, Worksheet } from '@univerjs/core'; +import type { ICommand, IMutationInfo, IRange } from '@univerjs/core'; import { CommandType, Dimension, @@ -6,14 +6,9 @@ import { IUndoRedoService, IUniverInstanceService, LocaleService, - ObjectMatrix, sequenceExecute, } from '@univerjs/core'; -import type { - IAddWorksheetMergeMutationParams, - IRemoveWorksheetMergeMutationParams, - ISetRangeValuesMutationParams, -} from '@univerjs/sheets'; +import type { IAddWorksheetMergeMutationParams, IRemoveWorksheetMergeMutationParams } from '@univerjs/sheets'; import { AddMergeUndoMutationFactory, AddWorksheetMergeMutation, @@ -21,12 +16,12 @@ import { RemoveMergeUndoMutationFactory, RemoveWorksheetMergeMutation, SelectionManagerService, - SetRangeValuesMutation, - SetRangeValuesUndoMutationFactory, } from '@univerjs/sheets'; import { IConfirmService } from '@univerjs/ui'; import type { IAccessor } from '@wendellhu/redi'; +import { checkCellContentInRanges, getClearContentMutationParamsForRanges } from '../../common/utils'; + export interface IAddMergeCommandParams { value?: Dimension.ROWS | Dimension.COLUMNS; selections: IRange[]; @@ -34,76 +29,6 @@ export interface IAddMergeCommandParams { worksheetId: string; } -function checkCellContentInRanges(worksheet: Worksheet, ranges: IRange[]): boolean { - return ranges.some((range) => checkCellContentInRange(worksheet, range)); -} - -function checkCellContentInRange(worksheet: Worksheet, range: IRange): boolean { - const { startRow, startColumn, endColumn, endRow } = range; - const cellMatrix = worksheet.getMatrixWithMergedCells(startRow, startColumn, endRow, endColumn); - - let someCellGoingToBeRemoved = false; - cellMatrix.forValue((row, col, cellData) => { - if (cellData && (row !== startRow || col !== startColumn) && worksheet.cellHasValue(cellData)) { - someCellGoingToBeRemoved = true; - return false; - } - }); - return someCellGoingToBeRemoved; -} - -function getClearContentMutationParamsForRanges( - accessor: IAccessor, - workbookId: string, - worksheet: Worksheet, - ranges: IRange[] -): { - undos: IMutationInfo[]; - redos: IMutationInfo[]; -} { - const undos: IMutationInfo[] = []; - const redos: IMutationInfo[] = []; - - const worksheetId = worksheet.getSheetId(); - - // Use the following file as a reference. - // packages/sheets/src/commands/commands/clear-selection-all.command.ts - // packages/sheets/src/commands/mutations/set-range-values.mutation.ts - ranges.forEach((range) => { - const redoMatrix = getClearContentMutationParamForRange(worksheet, range); - const redoMutationParams: ISetRangeValuesMutationParams = { - workbookId, - worksheetId, - cellValue: redoMatrix.getData(), - }; - const undoMutationParams: ISetRangeValuesMutationParams = SetRangeValuesUndoMutationFactory( - accessor, - redoMutationParams - ); - - undos.push({ id: SetRangeValuesMutation.id, params: undoMutationParams }); - redos.push({ id: SetRangeValuesMutation.id, params: redoMutationParams }); - }); - - return { - undos, - redos, - }; -} - -function getClearContentMutationParamForRange(worksheet: Worksheet, range: IRange): ObjectMatrix { - const { startRow, startColumn, endColumn, endRow } = range; - const cellMatrix = worksheet.getMatrixWithMergedCells(startRow, startColumn, endRow, endColumn); - const redoMatrix = new ObjectMatrix(); - cellMatrix.forValue((row, col, cellData) => { - if (cellData && (row !== startRow || col !== startColumn)) { - redoMatrix.setValue(row, col, null); - } - }); - - return redoMatrix; -} - export const AddWorksheetMergeCommand: ICommand = { type: CommandType.COMMAND, id: 'sheet.command.add-worksheet-merge', diff --git a/packages/sheets-ui/src/commands/commands/auto-fill.command.ts b/packages/sheets-ui/src/commands/commands/auto-fill.command.ts index e95f7f4fd4..fa47b3b9d6 100644 --- a/packages/sheets-ui/src/commands/commands/auto-fill.command.ts +++ b/packages/sheets-ui/src/commands/commands/auto-fill.command.ts @@ -107,7 +107,7 @@ export const AutoFillCommand: ICommand = { let addMergeResult = true; // add worksheet merge - if (applyMergeRanges) { + if (applyMergeRanges?.length) { const ranges = getAddMergeMutationRangeByType(applyMergeRanges); const removeMergeMutationParams: IRemoveWorksheetMergeMutationParams = { workbookId, diff --git a/packages/sheets-ui/src/commands/commands/set-format-painter.command.ts b/packages/sheets-ui/src/commands/commands/set-format-painter.command.ts index bbdd1d703d..39085cdfbc 100644 --- a/packages/sheets-ui/src/commands/commands/set-format-painter.command.ts +++ b/packages/sheets-ui/src/commands/commands/set-format-painter.command.ts @@ -1,7 +1,35 @@ -import type { ICommand } from '@univerjs/core'; -import { CommandType, ICommandService } from '@univerjs/core'; +import type { ICellData, ICommand, IMutationInfo, IRange, ObjectMatrixPrimitiveType } from '@univerjs/core'; +import { + CommandType, + ICommandService, + isICellData, + IUndoRedoService, + IUniverInstanceService, + ObjectMatrix, + sequenceExecute, + Tools, +} from '@univerjs/core'; +import type { + IAddWorksheetMergeMutationParams, + IRemoveWorksheetMergeMutationParams, + ISetRangeValuesMutationParams, +} from '@univerjs/sheets'; +import { + AddMergeUndoMutationFactory, + AddWorksheetMergeMutation, + getAddMergeMutationRangeByType, + INTERCEPTOR_POINT, + RemoveMergeUndoMutationFactory, + RemoveWorksheetMergeMutation, + SelectionManagerService, + SetRangeValuesCommand, + SetRangeValuesMutation, + SetRangeValuesUndoMutationFactory, + SheetInterceptorService, +} from '@univerjs/sheets'; import type { IAccessor } from '@wendellhu/redi'; +import { checkCellContentInRanges, getClearContentMutationParamsForRanges } from '../../common/utils'; import { FormatPainterStatus, IFormatPainterService } from '../../services/format-painter/format-painter.service'; import { SetFormatPainterOperation } from '../operations/set-format-painter.operation'; @@ -43,3 +71,146 @@ export const SetOnceFormatPainterCommand: ICommand = { return commandService.executeCommand(SetFormatPainterOperation.id, { status: newStatus }); }, }; + +export interface IApplyFormatPainterCommandParams { + worksheetId: string; + workbookId: string; + styleRange: IRange; + styleValues: ICellData[][]; + mergeRanges: IRange[]; +} + +export const ApplyFormatPainterCommand: ICommand = { + type: CommandType.COMMAND, + id: 'sheet.command.apply-format-painter', + handler: async (accessor: IAccessor, params: IApplyFormatPainterCommandParams) => { + const commandService = accessor.get(ICommandService); + const undoRedoService = accessor.get(IUndoRedoService); + const univerInstanceService = accessor.get(IUniverInstanceService); + const selectionManagerService = accessor.get(SelectionManagerService); + const sheetInterceptorService = accessor.get(SheetInterceptorService); + const { + styleValues: value, + styleRange: range, + mergeRanges, + workbookId = univerInstanceService.getCurrentUniverSheetInstance().getUnitId(), + worksheetId = univerInstanceService.getCurrentUniverSheetInstance().getActiveSheet().getSheetId(), + } = params; + + const currentSelections = range ? [range] : selectionManagerService.getSelectionRanges(); + if (!currentSelections || !currentSelections.length) { + return false; + } + + const cellValue = new ObjectMatrix(); + let realCellValue: ObjectMatrixPrimitiveType | undefined; + + if (Tools.isArray(value)) { + for (let i = 0; i < currentSelections.length; i++) { + const { startRow, startColumn, endRow, endColumn } = currentSelections[i]; + + for (let r = 0; r <= endRow - startRow; r++) { + for (let c = 0; c <= endColumn - startColumn; c++) { + cellValue.setValue(r + startRow, c + startColumn, value[r][c]); + } + } + } + } else if (isICellData(value)) { + for (let i = 0; i < currentSelections.length; i++) { + const { startRow, startColumn } = currentSelections[i]; + + cellValue.setValue(startRow, startColumn, value); + } + } else { + realCellValue = value as ObjectMatrixPrimitiveType; + } + + const setRangeValuesMutationParams: ISetRangeValuesMutationParams = { + worksheetId, + workbookId, + cellValue: realCellValue ?? cellValue.getMatrix(), + }; + const undoSetRangeValuesMutationParams: ISetRangeValuesMutationParams = SetRangeValuesUndoMutationFactory( + accessor, + setRangeValuesMutationParams + ); + + if ( + !sheetInterceptorService.fetchThroughInterceptors(INTERCEPTOR_POINT.PERMISSION)(null, { + id: SetRangeValuesCommand.id, + params: setRangeValuesMutationParams, + }) + ) { + return false; + } + + const setValueMutationResult = commandService.syncExecuteCommand( + SetRangeValuesMutation.id, + setRangeValuesMutationParams + ); + + const { undos: interceptorUndos, redos: interceptorRedos } = sheetInterceptorService.onCommandExecute({ + id: SetRangeValuesCommand.id, + params: { ...setRangeValuesMutationParams, range: currentSelections }, + }); + + // handle merge + const ranges = getAddMergeMutationRangeByType(mergeRanges); + const worksheet = univerInstanceService.getUniverSheetInstance(workbookId)!.getSheetBySheetId(worksheetId)!; + + const mergeRedos: IMutationInfo[] = []; + const mergeUndos: IMutationInfo[] = []; + + // First we should check if there are values in the going-to-be-merged cells. + const willRemoveSomeCell = checkCellContentInRanges(worksheet, ranges); + + // prepare redo mutations + const removeMergeMutationParams: IRemoveWorksheetMergeMutationParams = { + workbookId, + worksheetId, + ranges, + }; + const addMergeMutationParams: IAddWorksheetMergeMutationParams = { + workbookId, + worksheetId, + ranges, + }; + mergeRedos.push({ id: RemoveWorksheetMergeMutation.id, params: removeMergeMutationParams }); + mergeRedos.push({ id: AddWorksheetMergeMutation.id, params: addMergeMutationParams }); + + // prepare undo mutations + const undoRemoveMergeMutationParams = RemoveMergeUndoMutationFactory(accessor, removeMergeMutationParams); + const undoMutationParams = AddMergeUndoMutationFactory(accessor, addMergeMutationParams); + mergeUndos.push({ id: RemoveWorksheetMergeMutation.id, params: undoMutationParams }); + mergeUndos.push({ id: AddWorksheetMergeMutation.id, params: undoRemoveMergeMutationParams }); + + // add set range values mutations to undo redo mutations + if (willRemoveSomeCell) { + const data = getClearContentMutationParamsForRanges(accessor, workbookId, worksheet, ranges); + mergeRedos.unshift(...data.redos); + mergeUndos.push(...data.undos); + } + + const result = await sequenceExecute([...interceptorRedos, ...mergeRedos], commandService); + + if (setValueMutationResult && result.result) { + undoRedoService.pushUndoRedo({ + unitID: workbookId, + undoMutations: [ + { id: SetRangeValuesMutation.id, params: undoSetRangeValuesMutationParams }, + ...interceptorUndos, + ...mergeUndos, + ], + redoMutations: [ + { id: SetRangeValuesMutation.id, params: setRangeValuesMutationParams }, + ...interceptorRedos, + ...mergeRedos, + ], + }); + + return true; + } + + return false; + }, +}; diff --git a/packages/sheets-ui/src/common/utils.ts b/packages/sheets-ui/src/common/utils.ts new file mode 100644 index 0000000000..63425183d4 --- /dev/null +++ b/packages/sheets-ui/src/common/utils.ts @@ -0,0 +1,75 @@ +import type { ICellData, IMutationInfo, IRange, Worksheet } from '@univerjs/core'; +import { ObjectMatrix } from '@univerjs/core'; +import type { ISetRangeValuesMutationParams } from '@univerjs/sheets'; +import { SetRangeValuesMutation, SetRangeValuesUndoMutationFactory } from '@univerjs/sheets'; +import type { IAccessor } from '@wendellhu/redi'; + +export function checkCellContentInRanges(worksheet: Worksheet, ranges: IRange[]): boolean { + return ranges.some((range) => checkCellContentInRange(worksheet, range)); +} + +export function checkCellContentInRange(worksheet: Worksheet, range: IRange): boolean { + const { startRow, startColumn, endColumn, endRow } = range; + const cellMatrix = worksheet.getMatrixWithMergedCells(startRow, startColumn, endRow, endColumn); + + let someCellGoingToBeRemoved = false; + cellMatrix.forValue((row, col, cellData) => { + if (cellData && (row !== startRow || col !== startColumn) && worksheet.cellHasValue(cellData)) { + someCellGoingToBeRemoved = true; + return false; + } + }); + return someCellGoingToBeRemoved; +} + +export function getClearContentMutationParamsForRanges( + accessor: IAccessor, + workbookId: string, + worksheet: Worksheet, + ranges: IRange[] +): { + undos: IMutationInfo[]; + redos: IMutationInfo[]; +} { + const undos: IMutationInfo[] = []; + const redos: IMutationInfo[] = []; + + const worksheetId = worksheet.getSheetId(); + + // Use the following file as a reference. + // packages/sheets/src/commands/commands/clear-selection-all.command.ts + // packages/sheets/src/commands/mutations/set-range-values.mutation.ts + ranges.forEach((range) => { + const redoMatrix = getClearContentMutationParamForRange(worksheet, range); + const redoMutationParams: ISetRangeValuesMutationParams = { + workbookId, + worksheetId, + cellValue: redoMatrix.getData(), + }; + const undoMutationParams: ISetRangeValuesMutationParams = SetRangeValuesUndoMutationFactory( + accessor, + redoMutationParams + ); + + undos.push({ id: SetRangeValuesMutation.id, params: undoMutationParams }); + redos.push({ id: SetRangeValuesMutation.id, params: redoMutationParams }); + }); + + return { + undos, + redos, + }; +} + +export function getClearContentMutationParamForRange(worksheet: Worksheet, range: IRange): ObjectMatrix { + const { startRow, startColumn, endColumn, endRow } = range; + const cellMatrix = worksheet.getMatrixWithMergedCells(startRow, startColumn, endRow, endColumn); + const redoMatrix = new ObjectMatrix(); + cellMatrix.forValue((row, col, cellData) => { + if (cellData && (row !== startRow || col !== startColumn)) { + redoMatrix.setValue(row, col, null); + } + }); + + return redoMatrix; +} diff --git a/packages/sheets-ui/src/controllers/format-painter/format-painter.controller.ts b/packages/sheets-ui/src/controllers/format-painter/format-painter.controller.ts index 07e9ca9e46..40748e5289 100644 --- a/packages/sheets-ui/src/controllers/format-painter/format-painter.controller.ts +++ b/packages/sheets-ui/src/controllers/format-painter/format-painter.controller.ts @@ -1,19 +1,24 @@ import type { ICellData, ICommandInfo, IRange } from '@univerjs/core'; import { Disposable, ICommandService, IUniverInstanceService, LifecycleStages, OnLifecycle } from '@univerjs/core'; import { CURSOR_TYPE, IRenderManagerService } from '@univerjs/engine-render'; -import type { ISetRangeValuesCommandParams, ISetSelectionsOperationParams } from '@univerjs/sheets'; -import { SelectionManagerService, SetRangeValuesCommand, SetSelectionsOperation } from '@univerjs/sheets'; +import type { ISetSelectionsOperationParams } from '@univerjs/sheets'; +import { SelectionManagerService, SetSelectionsOperation } from '@univerjs/sheets'; import { Inject } from '@wendellhu/redi'; -import { SetOnceFormatPainterCommand } from '../../commands/commands/set-format-painter.command'; +import type { IApplyFormatPainterCommandParams } from '../../commands/commands/set-format-painter.command'; +import { + ApplyFormatPainterCommand, + SetOnceFormatPainterCommand, +} from '../../commands/commands/set-format-painter.command'; import { FormatPainterStatus, IFormatPainterService } from '../../services/format-painter/format-painter.service'; +import { getSheetObject } from '../utils/component-tools'; @OnLifecycle(LifecycleStages.Rendered, FormatPainterController) export class FormatPainterController extends Disposable { constructor( @ICommandService private readonly _commandService: ICommandService, @IFormatPainterService private readonly _formatPainterService: IFormatPainterService, - @IUniverInstanceService private readonly _currentService: IUniverInstanceService, + @IUniverInstanceService private readonly _currentUniverService: IUniverInstanceService, @IRenderManagerService private readonly _renderManagerService: IRenderManagerService, @Inject(SelectionManagerService) private readonly _selectionManagerService: SelectionManagerService ) { @@ -29,7 +34,7 @@ export class FormatPainterController extends Disposable { private _bindFormatPainterStatus() { this._formatPainterService.status$.subscribe((status) => { - const scene = this._renderManagerService.getCurrent()?.scene; + const { scene } = this._getSheetObject() || {}; if (!scene) return; if (status !== FormatPainterStatus.OFF) { scene.setDefaultCursor(CURSOR_TYPE.CELL); @@ -58,10 +63,10 @@ export class FormatPainterController extends Disposable { ); } - private _applyFormatPainter(range: IRange) { - const stylesMatrix = this._formatPainterService.getSelectionStyles(); - const workbookId = this._currentService.getCurrentUniverSheetInstance().getUnitId(); - const worksheetId = this._currentService.getCurrentUniverSheetInstance().getActiveSheet().getSheetId(); + private async _applyFormatPainter(range: IRange) { + const { styles: stylesMatrix, merges } = this._formatPainterService.getSelectionFormat(); + const workbookId = this._currentUniverService.getCurrentUniverSheetInstance().getUnitId(); + const worksheetId = this._currentUniverService.getCurrentUniverSheetInstance().getActiveSheet().getSheetId(); if (!stylesMatrix) return; const { startRow, startColumn, endRow, endColumn } = stylesMatrix.getDataRange(); @@ -70,6 +75,7 @@ export class FormatPainterController extends Disposable { const styleValues: ICellData[][] = Array.from({ length: range.endRow - range.startRow + 1 }, () => Array.from({ length: range.endColumn - range.startColumn + 1 }, () => ({})) ); + const mergeRanges: IRange[] = []; styleValues.forEach((row, rowIndex) => { row.forEach((col, colIndex) => { @@ -83,13 +89,39 @@ export class FormatPainterController extends Disposable { }); }); - const setRangeValuesCommandParams: ISetRangeValuesCommandParams = { + merges.forEach((merge) => { + const relatedRange: IRange = { + startRow: merge.startRow - startRow, + startColumn: merge.startColumn - startColumn, + endRow: merge.endRow - startRow, + endColumn: merge.endColumn - startColumn, + }; + const rowRepeats = Math.floor((range.endRow - range.startRow + 1) / styleRowsNum); + const colRepeats = Math.floor((range.endColumn - range.startColumn + 1) / styleColsNum); + for (let i = 0; i < rowRepeats; i++) { + for (let j = 0; j < colRepeats; j++) { + mergeRanges.push({ + startRow: relatedRange.startRow + i * styleRowsNum + range.startRow, + startColumn: relatedRange.startColumn + j * styleColsNum + range.startColumn, + endRow: relatedRange.endRow + i * styleRowsNum + range.startRow, + endColumn: relatedRange.endColumn + j * styleColsNum + range.startColumn, + }); + } + } + }); + + const ApplyFormatPainterCommandParams: IApplyFormatPainterCommandParams = { worksheetId, workbookId, - range, - value: styleValues, + styleRange: range, + styleValues, + mergeRanges, }; - this._commandService.executeCommand(SetRangeValuesCommand.id, setRangeValuesCommandParams); + await this._commandService.executeCommand(ApplyFormatPainterCommand.id, ApplyFormatPainterCommandParams); + } + + private _getSheetObject() { + return getSheetObject(this._currentUniverService, this._renderManagerService); } } diff --git a/packages/sheets-ui/src/controllers/sheet-ui.controller.ts b/packages/sheets-ui/src/controllers/sheet-ui.controller.ts index 05750eb933..f8bc7e9de0 100644 --- a/packages/sheets-ui/src/controllers/sheet-ui.controller.ts +++ b/packages/sheets-ui/src/controllers/sheet-ui.controller.ts @@ -31,6 +31,7 @@ import { import { RefillCommand } from '../commands/commands/refill.command'; import { RenameSheetOperation } from '../commands/commands/rename.command'; import { + ApplyFormatPainterCommand, SetInfiniteFormatPainterCommand, SetOnceFormatPainterCommand, } from '../commands/commands/set-format-painter.command'; @@ -247,6 +248,7 @@ export class SheetUIController extends Disposable { SetFormatPainterOperation, SetInfiniteFormatPainterCommand, SetOnceFormatPainterCommand, + ApplyFormatPainterCommand, SetScrollOperation, SetScrollRelativeCommand, SetSelectionFrozenCommand, diff --git a/packages/sheets-ui/src/services/clipboard/clipboard.service.ts b/packages/sheets-ui/src/services/clipboard/clipboard.service.ts index 16480affb9..eb306c83db 100644 --- a/packages/sheets-ui/src/services/clipboard/clipboard.service.ts +++ b/packages/sheets-ui/src/services/clipboard/clipboard.service.ts @@ -154,10 +154,9 @@ export class SheetClipboardService extends Disposable implements ISheetClipboard await this._clipboardInterfaceService.write(plain, html); // 9. mark the copy range + this._markSelectionService.removeAllShapes(); + const style = this._selectionManagerService.createCopyPasteSelection(); - if (this._copyMarkId) { - this._markSelectionService.removeShape(this._copyMarkId); - } this._copyMarkId = this._markSelectionService.addShape({ ...selection, style }); // tell hooks to clean up diff --git a/packages/sheets-ui/src/services/format-painter/format-painter.service.ts b/packages/sheets-ui/src/services/format-painter/format-painter.service.ts index 3ec284373a..ca7099181e 100644 --- a/packages/sheets-ui/src/services/format-painter/format-painter.service.ts +++ b/packages/sheets-ui/src/services/format-painter/format-painter.service.ts @@ -1,5 +1,6 @@ -import type { ICellData, IStyleData } from '@univerjs/core'; +import type { ICellData, IRange, IStyleData } from '@univerjs/core'; import { Disposable, IUniverInstanceService, ObjectMatrix } from '@univerjs/core'; +import { getCellInfoInMergeData } from '@univerjs/engine-render'; import { SelectionManagerService } from '@univerjs/sheets'; import { createIdentifier, Inject } from '@wendellhu/redi'; import type { Observable } from 'rxjs'; @@ -11,9 +12,13 @@ export interface IFormatPainterService { status$: Observable; setStatus(status: FormatPainterStatus): void; getStatus(): FormatPainterStatus; - getSelectionStyles(): ObjectMatrix; + getSelectionFormat(): ISelectionFormatInfo; } +export interface ISelectionFormatInfo { + styles: ObjectMatrix; + merges: IRange[]; +} export enum FormatPainterStatus { OFF, ONCE, @@ -24,25 +29,25 @@ export const IFormatPainterService = createIdentifier('un export class FormatPainterService extends Disposable implements IFormatPainterService { readonly status$: Observable; - private _selectionStyles: ObjectMatrix; + private _selectionFormat: ISelectionFormatInfo; private _markId: string | null = null; private readonly _status$: BehaviorSubject; constructor( @Inject(SelectionManagerService) private readonly _selectionManagerService: SelectionManagerService, - @IUniverInstanceService private readonly _currentService: IUniverInstanceService, + @IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService, @IMarkSelectionService private readonly _markSelectionService: IMarkSelectionService ) { super(); this._status$ = new BehaviorSubject(FormatPainterStatus.OFF); this.status$ = this._status$.asObservable(); - this._selectionStyles = new ObjectMatrix(); + this._selectionFormat = { styles: new ObjectMatrix(), merges: [] }; } setStatus(status: FormatPainterStatus) { if (status !== FormatPainterStatus.OFF) { - this._getSelectionRangeStyle(); + this._getSelectionRangeFormat(); } this._updateRangeMark(status); this._status$.next(status); @@ -53,10 +58,9 @@ export class FormatPainterService extends Disposable implements IFormatPainterSe } private _updateRangeMark(status: FormatPainterStatus) { - if (this._markId) { - this._markSelectionService.removeShape(this._markId); - this._markId = null; - } + this._markSelectionService.removeAllShapes(); + this._markId = null; + if (status !== FormatPainterStatus.OFF) { const selection = this._selectionManagerService.getLast(); if (selection) { @@ -66,15 +70,15 @@ export class FormatPainterService extends Disposable implements IFormatPainterSe } } - private _getSelectionRangeStyle() { + private _getSelectionRangeFormat() { const selection = this._selectionManagerService.getLast(); const range = selection?.range; if (!range) return; const { startRow, endRow, startColumn, endColumn } = range; - const workbook = this._currentService.getCurrentUniverSheetInstance(); + const workbook = this._univerInstanceService.getCurrentUniverSheetInstance(); const worksheet = workbook?.getActiveSheet(); const cellData = worksheet.getCellMatrix(); - // const value = cellData.getFragments(startRow, endRow, startColumn, endColumn); + const mergeData = this._univerInstanceService.getCurrentUniverSheetInstance().getActiveSheet().getMergeData(); const styles = workbook.getStyles(); const stylesMatrix = new ObjectMatrix(); @@ -82,12 +86,21 @@ export class FormatPainterService extends Disposable implements IFormatPainterSe for (let c = startColumn; c <= endColumn; c++) { const cell = cellData.getValue(r, c) as ICellData; stylesMatrix.setValue(r, c, styles.getStyleByCell(cell) || {}); + const { isMergedMainCell, ...mergeInfo } = getCellInfoInMergeData(r, c, mergeData); + if (isMergedMainCell) { + this._selectionFormat.merges.push({ + startRow: mergeInfo.startRow, + startColumn: mergeInfo.startColumn, + endRow: mergeInfo.endRow, + endColumn: mergeInfo.endColumn, + }); + } } } - this._selectionStyles = stylesMatrix; + this._selectionFormat.styles = stylesMatrix; } - getSelectionStyles() { - return this._selectionStyles; + getSelectionFormat() { + return this._selectionFormat; } } diff --git a/packages/sheets-ui/src/services/mark-selection/mark-selection.service.ts b/packages/sheets-ui/src/services/mark-selection/mark-selection.service.ts index 0bd83fba15..f6123450ec 100644 --- a/packages/sheets-ui/src/services/mark-selection/mark-selection.service.ts +++ b/packages/sheets-ui/src/services/mark-selection/mark-selection.service.ts @@ -2,7 +2,7 @@ import type { ICommandInfo } from '@univerjs/core'; import { Disposable, ICommandService, IUniverInstanceService, ThemeService, Tools } from '@univerjs/core'; import { IRenderManagerService } from '@univerjs/engine-render'; import type { ISelectionWithStyle } from '@univerjs/sheets'; -import { SelectionManagerService, SetWorksheetActivateMutation } from '@univerjs/sheets'; +import { SelectionManagerService, SetRangeValuesMutation, SetWorksheetActivateMutation } from '@univerjs/sheets'; import { createIdentifier, Inject } from '@wendellhu/redi'; import { SetCellEditVisibleOperation } from '../../commands/operations/cell-edit.operation'; @@ -100,7 +100,7 @@ export class MarkSelectionService extends Disposable implements IMarkSelectionSe } private _addRemoveListener() { - const removeCommands = [SetCellEditVisibleOperation.id]; + const removeCommands = [SetCellEditVisibleOperation.id, SetRangeValuesMutation.id]; this.disposeWithMe( this._commandService.onCommandExecuted((command: ICommandInfo) => { if (removeCommands.includes(command.id)) { diff --git a/packages/sheets/src/services/selection-manager.service.ts b/packages/sheets/src/services/selection-manager.service.ts index cb4ae1cda6..ed0550f7e3 100644 --- a/packages/sheets/src/services/selection-manager.service.ts +++ b/packages/sheets/src/services/selection-manager.service.ts @@ -1,5 +1,5 @@ -import type { IRange, ISelectionCell, Nullable } from '@univerjs/core'; -import type { IDisposable } from '@wendellhu/redi'; +import { type IRange, type ISelectionCell, type Nullable, ThemeService } from '@univerjs/core'; +import { type IDisposable, Inject } from '@wendellhu/redi'; import { BehaviorSubject, Subject } from 'rxjs'; import type { ISelectionStyle, ISelectionWithStyle } from '../basics/selection'; @@ -58,6 +58,16 @@ export class SelectionManagerService implements IDisposable { private _dirty: boolean = true; + // get isSelectionEnabled() { + // return this._isSelectionEnabled; + // } + + // get currentStyle() { + // return this._currentStyle; + // } + + constructor(@Inject(ThemeService) private readonly _themeService: ThemeService) {} + getCurrent() { return this._currentSelection; } @@ -258,8 +268,8 @@ export class SelectionManagerService implements IDisposable { createCopyPasteSelection(): ISelectionStyle { return { strokeWidth: 2, - stroke: '#FFF000', - fill: 'rgba(0, 0, 0, 0.2)', + stroke: this._themeService.getCurrentTheme().primaryColor, + fill: 'rgba(178, 178, 178, 0.10)', widgets: {}, hasAutoFill: false, strokeDash: 8,