From 3d343c63f4c4c46a8da68e9a67aa7cfee551e482 Mon Sep 17 00:00:00 2001 From: Igor Zinken Date: Tue, 12 Jan 2021 17:15:24 +0100 Subject: [PATCH] Added layer interaction to state history --- .../application-menu/application-menu.vue | 22 +++++- .../options-panel/add-layer/add-layer.vue | 26 +++++-- .../options-panel/layers/layers.vue | 77 +++++++++++++------ src/store/index.js | 22 +++++- src/store/modules/document-module.js | 5 ++ .../store/modules/document-module.spec.js | 14 ++++ tests/unit/store/store.spec.js | 18 ++--- 7 files changed, 138 insertions(+), 46 deletions(-) diff --git a/src/components/application-menu/application-menu.vue b/src/components/application-menu/application-menu.vue index 098e3c5..d05fec3 100644 --- a/src/components/application-menu/application-menu.vue +++ b/src/components/application-menu/application-menu.vue @@ -202,6 +202,7 @@ import { import { supportsFullscreen, setToggleButton } from "@/utils/environment-util"; import { getRectangleForSelection } from "@/math/selection-math"; import { getCanvasInstance, runSpriteFn, getSpriteForLayer } from "@/factories/sprite-factory"; +import { enqueueState } from "@/factories/history-state-factory"; import messages from "./messages.json"; export default { @@ -283,10 +284,25 @@ export default { requestSelectionSave() { this.openModal( SAVE_SELECTION ); }, - async requestCropToSelection() { + requestCropToSelection() { + const store = this.$store; + const currentSize = { + width: this.activeDocument.width, + height: this.activeDocument.height + }; const { left, top, width, height } = getRectangleForSelection( this.activeLayer.selection ); - await this.cropActiveDocumentContent({ left, top }); - this.setActiveDocumentSize({ width, height }); + const commit = async () => { + await store.commit( "cropActiveDocumentContent", { left, top }); + store.commit( "setActiveDocumentSize", { width, height }); + }; + commit(); + enqueueState("crop", { + async undo() { + await store.commit( "cropActiveDocumentContent", { left: -left, top: -top }); + store.commit( "setActiveDocumentSize", currentSize ); + }, + redo: commit + }); }, requestDropboxLoad() { this.openModal( DROPBOX_FILE_SELECTOR ); diff --git a/src/components/options-panel/add-layer/add-layer.vue b/src/components/options-panel/add-layer/add-layer.vue index 7e06d8c..13a2395 100644 --- a/src/components/options-panel/add-layer/add-layer.vue +++ b/src/components/options-panel/add-layer/add-layer.vue @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Igor Zinken 2020 - https://www.igorski.nl + * Igor Zinken 2020-2021 - https://www.igorski.nl * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in @@ -64,6 +64,7 @@ import { mapGetters, mapMutations, mapActions } from "vuex"; import Modal from "@/components/modal/modal"; import SelectBox from '@/components/ui/select-box/select-box'; +import { enqueueState } from "@/factories/history-state-factory"; import { LAYER_GRAPHIC, LAYER_TEXT } from "@/definitions/layer-types"; import messages from "./messages.json"; @@ -80,6 +81,7 @@ export default { computed: { ...mapGetters([ "activeDocument", + "activeLayerIndex", "layers", ]), layerTypes() { @@ -98,18 +100,28 @@ export default { }, methods: { ...mapMutations([ - "addLayer", "closeModal", ]), requestLayerAdd() { if ( !this.isValid ) { return; } - this.addLayer({ - name: this.name, - type: this.type, - width: this.activeDocument.width, - height: this.activeDocument.height + const newLayer = { + name : this.name, + type : this.type, + width : this.activeDocument.width, + height : this.activeDocument.height + }; + const store = this.$store; + const commit = () => store.commit( "addLayer", newLayer ); + commit(); + const addedLayerIndex = this.activeLayerIndex; + + enqueueState( "layerAdd", { + undo() { + store.commit( "removeLayer", addedLayerIndex ); + }, + redo: commit, }); this.closeModal(); }, diff --git a/src/components/options-panel/layers/layers.vue b/src/components/options-panel/layers/layers.vue index ec93382..2fe99da 100644 --- a/src/components/options-panel/layers/layers.vue +++ b/src/components/options-panel/layers/layers.vue @@ -116,6 +116,7 @@ import { mapGetters, mapMutations } from "vuex"; import { ADD_LAYER, LAYER_FILTERS } from "@/definitions/modal-windows"; import { createCanvas } from "@/utils/canvas-util"; import { getSpriteForLayer } from "@/factories/sprite-factory"; +import { enqueueState } from "@/factories/history-state-factory"; import KeyboardService from "@/services/keyboard-service"; import messages from "./messages.json"; @@ -153,7 +154,6 @@ export default { ...mapMutations([ "openModal", "removeLayer", - "updateLayer", "setActiveLayerIndex", "setActiveLayerMask", "openDialog", @@ -161,31 +161,33 @@ export default { requestLayerAdd() { this.openModal( ADD_LAYER ); }, - requestMaskAdd() { - this.updateLayer({ - index: this.activeLayerIndex, - opts: { - mask: createCanvas( this.activeLayer.width, this.activeLayer.height ).cvs - } - }); - }, handleLayerDoubleClick( index ) { this.editable = true; }, updateActiveLayerName({ target }) { - this.updateLayer({ - index: this.activeLayerIndex, - opts: { - name: target.value - } + const newName = target.value; + const currentName = this.activeLayer.name; + const index = this.activeLayerIndex; + const store = this.$store; + const commit = () => store.commit( "updateLayer", { index, opts: { name: newName } }); + commit(); + enqueueState( `layerName_${index}`, { + undo() { + store.commit( "updateLayer", { index, opts: { name: currentName } }); + }, + redo: commit, }); }, toggleLayerVisibility( index ) { - this.updateLayer({ - index, - opts: { - visible: !this.layers[ index ].visible - } + const originalVisibility = this.layers[ index ].visible; + const store = this.$store; + const commit = () => store.commit( "updateLayer", { index, opts: { visible: !originalVisibility } }); + commit(); + enqueueState( `layerVisibility_${index}`, { + undo() { + store.commit( "updateLayer", { index, opts: { visible: originalVisibility } }); + }, + redo: commit, }); }, handleFiltersClick( index ) { @@ -201,22 +203,53 @@ export default { } }, requestLayerRemove( index ) { + const layer = this.layers[ index ]; this.openDialog({ type: "confirm", title: this.$t( "areYouSure" ), - message: this.$t( "doYouWantToRemoveLayerName", { name: this.layers[ index ]?.name }), + message: this.$t( "doYouWantToRemoveLayerName", { name: layer.name }), confirm: () => { - this.removeLayer( index ); + const store = this.$store; + const commit = () => store.commit( "removeLayer", index ); + commit(); + enqueueState( `layerRemove_${index}`, { + undo() { + store.commit( "insertLayerAtIndex", { index, layer }); + }, + redo: commit, + }); } }); }, + requestMaskAdd() { + const index = this.activeLayerIndex; + const mask = createCanvas( this.activeLayer.width, this.activeLayer.height ).cvs; + const store = this.$store; + const commit = () => store.commit( "updateLayer", { index, opts: { mask } }); + commit(); + enqueueState( `maskAdd_${index}`, { + undo() { + store.commit( "updateLayer", { index, opts: { mask: null } }); + }, + redo: commit, + }); + }, requestMaskRemove( index ) { + const mask = this.activeLayer.mask; this.openDialog({ type: "confirm", title: this.$t( "areYouSure" ), message: this.$t( "doYouWantToRemoveMaskName", { name: this.layers[ index ]?.name }), confirm: () => { - this.updateLayer({ index, opts: { mask: null } }); + const store = this.$store; + const commit = () => store.commit( "updateLayer", { index, opts: { mask: null } }); + commit(); + enqueueState( `maskRemove_${index}`, { + undo() { + store.commit( "updateLayer", { index, opts: { mask } }); + }, + redo: commit, + }); } }); }, diff --git a/src/store/index.js b/src/store/index.js index e4a2dd3..7375456 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -22,7 +22,8 @@ */ import KeyboardService from "@/services/keyboard-service"; import DocumentFactory from "@/factories/document-factory"; -import { initHistory } from "@/factories/history-state-factory"; +import LayerFactory from "@/factories/layer-factory"; +import { initHistory, enqueueState } from "@/factories/history-state-factory"; import { LAYER_IMAGE } from "@/definitions/layer-types"; import { runSpriteFn } from "@/factories/sprite-factory"; import canvasModule from "./modules/canvas-module"; @@ -193,15 +194,28 @@ export default { runSpriteFn( sprite => sprite.resetSelection(), getters.activeDocument ); }, pasteSelection({ commit, getters, dispatch, state }) { - const { image, size } = state.selectionContent; - commit( "addLayer", { + const selection = state.selectionContent; + const { image, size } = selection; + const layer = LayerFactory.create({ type: LAYER_IMAGE, source: image, ...size, x: getters.activeDocument.width / 2 - size.width / 2, y: getters.activeDocument.height / 2 - size.height / 2, }); - dispatch( "clearSelection" ); + const index = getters.activeDocument.layers.length; + const paste = () => { + commit( "insertLayerAtIndex", { index, layer }); + dispatch( "clearSelection" ); + }; + paste(); + enqueueState( `paste_${selection.length}`, { + undo() { + commit( "setSelectionContent", selection ); + commit( "removeLayer", index ); + }, + redo: paste + }); }, /** * Install the services that will listen to global hardware events diff --git a/src/store/modules/document-module.js b/src/store/modules/document-module.js index ca764eb..f092e13 100644 --- a/src/store/modules/document-module.js +++ b/src/store/modules/document-module.js @@ -84,6 +84,11 @@ export default { layers.push( LayerFactory.create( opts )); state.activeLayerIndex = layers.length - 1; }, + insertLayerAtIndex( state, { index, layer }) { + const document = state.documents[ state.activeIndex ]; + document.layers.splice( index, 0, layer ); + state.activeLayerIndex = index; + }, removeLayer( state, index ) { const layer = state.documents[ state.activeIndex ]?.layers[ index ]; if ( !layer ) { diff --git a/tests/unit/store/modules/document-module.spec.js b/tests/unit/store/modules/document-module.spec.js index c691d85..eae994b 100644 --- a/tests/unit/store/modules/document-module.spec.js +++ b/tests/unit/store/modules/document-module.spec.js @@ -233,6 +233,20 @@ describe( "Vuex document module", () => { { name: "layer1" }, expect.any( Object ) ]); }); + + it( "should be able to add layers at specific indices in the layer list", () => { + const state = { + documents: [ { name: "foo", layers: [{ name: "layer1" }, { name: "layer2" }] }], + activeIndex: 0, + activeLayerIndex: 0, + }; + const layer = { name: "layer3" }; + mutations.insertLayerAtIndex( state, { index: 1, layer }); + expect( state.documents[0].layers).toEqual([ + { name: "layer1" }, layer, { name: "layer2" } + ]); + expect( state.activeLayerIndex ).toEqual( 1 ); + }); }); describe( "when removing layers", () => { diff --git a/tests/unit/store/store.spec.js b/tests/unit/store/store.spec.js index 10700c9..e69f991 100644 --- a/tests/unit/store/store.spec.js +++ b/tests/unit/store/store.spec.js @@ -16,7 +16,12 @@ jest.mock( "@/factories/document-factory", () => ({ jest.mock( "@/utils/file-util", () => ({ selectFile: (...args) => mockUpdateFn?.( "selectFile", ...args ), saveBlobAsFile: (...args) => mockUpdateFn?.( "saveBlobAsFile", ...args ), -})) +})); +jest.mock("@/utils/canvas-util", () => ({ + createCanvas: jest.fn(), + imageToBase64: jest.fn(), + base64ToLayerImage: jest.fn() +})); describe( "Vuex store", () => { describe( "getters", () => { @@ -270,18 +275,11 @@ describe( "Vuex store", () => { const state = { selectionContent: { image: { src: "foo" }, size: { width: 40, height: 30 } }, }; - const mockedGetters = { activeDocument: { width: 200, height: 150 } }; + const mockedGetters = { activeDocument: { width: 200, height: 150, layers: [] } }; const commit = jest.fn(); const dispatch = jest.fn(); await actions.pasteSelection({ state, getters: mockedGetters, commit, dispatch }); - expect( commit ).toHaveBeenCalledWith( "addLayer", { - type: LAYER_IMAGE, - source: state.selectionContent.image, - width: state.selectionContent.size.width, - height: state.selectionContent.size.height, - x: 80, - y: 60 - }); + expect( commit ).toHaveBeenCalledWith( "insertLayerAtIndex", { index: 0, layer: expect.any( Object ) }); expect( dispatch ).toHaveBeenCalledWith( "clearSelection" ); }); });