From 5b41fc576d9e0682312c244d9c7e15a670480ac9 Mon Sep 17 00:00:00 2001 From: Igor Zinken Date: Sun, 27 Feb 2022 08:26:48 +0100 Subject: [PATCH] Implemented clipboard pasting of audio and Efflux project files --- src/efflux-application.vue | 23 +++++++++-- src/services/keyboard-service.js | 5 +-- src/store/modules/selection-module.js | 12 ++++-- src/utils/file-util.js | 13 +++++- .../store/modules/selection-module.spec.js | 41 ++++++++++++------- 5 files changed, 68 insertions(+), 26 deletions(-) diff --git a/src/efflux-application.vue b/src/efflux-application.vue index 992b8b18..31041c72 100644 --- a/src/efflux-application.vue +++ b/src/efflux-application.vue @@ -119,7 +119,7 @@ import { loadSample } from "@/services/audio/sample-loader"; import PubSubService from "@/services/pubsub-service"; import PubSubMessages from "@/services/pubsub/messages"; import SampleFactory from "@/model/factories/sample-factory"; -import { readDroppedFiles } from "@/utils/file-util"; +import { readClipboardFiles, readDroppedFiles } from "@/utils/file-util"; import store from "@/store"; import messages from "@/messages.json"; @@ -187,6 +187,7 @@ export default { "activeSong", "displayHelp", "displayWelcome", + "hasChanges", "isLoading", "timelineMode", ]), @@ -323,11 +324,26 @@ export default { } } for ( const file of projects ) { - this.loadSong({ file }); - this.closeModal(); + const confirm = () => { + this.loadSong({ file }); + this.closeModal(); + }; + if ( this.hasChanges ) { + this.openDialog({ + type: "confirm", + message: this.$t( "warnings.loadNewPendingChanges" ), + confirm + }); + } else { + confirm(); + } } }; + window.addEventListener( "paste", event => { + loadFiles( readClipboardFiles( event?.clipboardData )); + }, false ); + window.addEventListener( "dragover", event => { event.stopPropagation(); event.preventDefault(); @@ -375,6 +391,7 @@ export default { "setWindowSize", "resetEditor", "resetHistory", + "openDialog", "openModal", "closeModal", "showNotification", diff --git a/src/services/keyboard-service.js b/src/services/keyboard-service.js index f624a1f8..adac8869 100644 --- a/src/services/keyboard-service.js +++ b/src/services/keyboard-service.js @@ -422,9 +422,8 @@ function handleKeyDown( event ) { case 86: // V // paste current selection - if ( hasOption ) { + if ( hasOption && store.getters.hasCopiedEvents ) { store.commit( "saveState", createAction( Actions.PASTE_SELECTION, { store })); - preventDefault( event ); // override browser paste } break; @@ -434,7 +433,7 @@ function handleKeyDown( event ) { if ( hasOption ) { store.commit( "saveState", createAction( Actions.CUT_SELECTION, { store })); - preventDefault(event); // override browser cut + preventDefault( event ); // override browser cut } break; diff --git a/src/store/modules/selection-module.js b/src/store/modules/selection-module.js index 6810580d..8226a8eb 100644 --- a/src/store/modules/selection-module.js +++ b/src/store/modules/selection-module.js @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * - * Igor Zinken 2016-2021 - https://www.igorski.nl + * Igor Zinken 2016-2022 - 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 @@ -67,7 +67,7 @@ const getSelectionLength = state => ( state.maxSelectedStep - state.minSelectedS * this model so it can later on be pasted from this model */ const copySelection = ( state, { song, activePattern, optOutputArray }) => { - if ( getSelectionLength(state) === 0 ) + if ( getSelectionLength( state ) === 0 ) return; let i, max = state.selectedChannels.length; @@ -80,7 +80,7 @@ const copySelection = ( state, { song, activePattern, optOutputArray }) => { for ( i = 0; i < max; ++i ) optOutputArray.push( [] ); - let pattern = song.patterns[activePattern], stepValue; + let pattern = song.patterns[ activePattern ], stepValue; let channel; let copyIndex = 0; @@ -96,6 +96,9 @@ const copySelection = ( state, { song, activePattern, optOutputArray }) => { ++copyIndex; } } + // by writing into the clipboard we ensure that copied files are no + // longer in the clipboard history (prevents double load on paste shortcut) + window.navigator.clipboard.writeText( JSON.stringify( optOutputArray )); }; /** @@ -254,7 +257,8 @@ const module = { const out = []; copySelection( state, { song, activePattern, out }); return out; - } + }, + hasCopiedEvents: state => !!state.copySelection?.length, }, mutations: { setMinSelectedStep( state, value ) { diff --git a/src/utils/file-util.js b/src/utils/file-util.js index 18f83d8f..e6fff78f 100644 --- a/src/utils/file-util.js +++ b/src/utils/file-util.js @@ -1,7 +1,7 @@ /** * The MIT License (MIT) * -* Igor Zinken 2019-2021 - https://www.igorski.nl +* Igor Zinken 2019-2022 - 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 @@ -121,6 +121,17 @@ export const openFileBrowser = ( handler, acceptedFileTypes ) => { fileBrowser.addEventListener( "change", handler ); }; +export const readClipboardFiles = clipboardData => { + const items = [ ...( clipboardData?.items || []) ] + .filter( item => item.kind === "file" ) + .map( item => item.getAsFile() ); + + return { + sounds : items.filter( isSoundFile ), + projects : items.filter( isProjectFile ) + }; +}; + export const readDroppedFiles = dataTransfer => { const items = [ ...( dataTransfer?.files || []) ]; return { diff --git a/tests/unit/store/modules/selection-module.spec.js b/tests/unit/store/modules/selection-module.spec.js index 411bd5d3..f8499a67 100644 --- a/tests/unit/store/modules/selection-module.spec.js +++ b/tests/unit/store/modules/selection-module.spec.js @@ -1,8 +1,8 @@ -import SelectionModule from '@/store/modules/selection-module'; +import SelectionModule from "@/store/modules/selection-module"; const { getters, mutations } = SelectionModule; -describe('Vuex selection module', () => { +describe("Vuex selection module", () => { let state; beforeEach(() => { state = { @@ -15,8 +15,8 @@ describe('Vuex selection module', () => { }; }); - describe('getters', () => { - it('should know the full length of its selection', () => { + describe("getters", () => { + it("should know the full length of its selection", () => { mutations.setSelectionChannelRange(state, { firstChannel: 0, lastChannel: 1 }); const min = 0; const max = 16; @@ -28,7 +28,7 @@ describe('Vuex selection module', () => { expect(expected).toEqual(getters.getSelectionLength(state)); }); - it('should know whether it has a selection', () => { + it("should know whether it has a selection", () => { expect(getters.hasSelection(state)).toBe(false); mutations.setSelectionChannelRange(state, { firstChannel: 0, lastChannel: 4 }); @@ -36,10 +36,21 @@ describe('Vuex selection module', () => { expect(getters.hasSelection(state)).toBe(true); }); + + it("should know whether there is a range of copied events in the selection state", () => { + const state = { copySelection: null }; + expect( getters.hasCopiedEvents( state )).toBe( false ); + + state.copySelection = []; + expect( getters.hasCopiedEvents( state )).toBe( false ); + + state.copySelection = [{ foo: "bar" }]; + expect( getters.hasCopiedEvents( state )).toBe( true ); + }); }); - describe('mutations', () => { - it('should be able to select multiple channels for its selection', () => { + describe("mutations", () => { + it("should be able to select multiple channels for its selection", () => { mutations.setSelectionChannelRange(state, { firstChannel: 0 }); expect(1).toEqual(state.selectedChannels.length); @@ -51,7 +62,7 @@ describe('Vuex selection module', () => { expect(3).toEqual(state.lastSelectedChannel); }); - it('should add indices to its current selection', () => { + it("should add indices to its current selection", () => { let min = 0, i; const max = 16; @@ -63,7 +74,7 @@ describe('Vuex selection module', () => { } }); - it('should know the minimum and maximum indices of its selection', () => { + it("should know the minimum and maximum indices of its selection", () => { mutations.setSelectionChannelRange(state, { firstChannel: 0 }); // select a single channel let min = 0; @@ -76,7 +87,7 @@ describe('Vuex selection module', () => { expect(max).toEqual(state.maxSelectedStep); }); - it('should add not add the same index twice to its current selection', () => { + it("should add not add the same index twice to its current selection", () => { const activeChannel = 0, max = 1; mutations.setSelectionChannelRange(state, { firstChannel: activeChannel, lastChannel: max }); @@ -89,7 +100,7 @@ describe('Vuex selection module', () => { expect(2).toEqual(state.selectedChannels[activeChannel].length); }); - it('should be able to clear its selection', () => { + it("should be able to clear its selection", () => { mutations.setSelectionChannelRange(state, { firstChannel: 0, lastChannel: 1 }); mutations.setSelection(state, { selectionStart: 0, selectionEnd: 1 }); @@ -101,7 +112,7 @@ describe('Vuex selection module', () => { expect(getters.hasSelection(state)).toBe(false); }); - it('should be able to equalize the selection for all channels', () => { + it("should be able to equalize the selection for all channels", () => { mutations.setSelectionChannelRange(state, { firstChannel: 0, lastChannel: 3 }); const activeChannel = 0; @@ -114,14 +125,14 @@ describe('Vuex selection module', () => { expect(JSON.stringify(state.selectedChannels[activeChannel])).toEqual(JSON.stringify(state.selectedChannels[otherChannel])); }); - it('should treat a single step as 1 unit range', () => { + it("should treat a single step as 1 unit range", () => { mutations.setSelectionChannelRange(state, { firstChannel: 0, lastChannel: 1 }); mutations.setSelection(state, { selectionStart: 0 }); expect(1).toEqual(getters.getSelectionLength(state)); }); - it('should be able to expand and shrink its selection when starting key selection to the right', () => { + it("should be able to expand and shrink its selection when starting key selection to the right", () => { let keyCode = 39; // right let selectedChannel = 2; const selectedStep = 1; @@ -163,7 +174,7 @@ describe('Vuex selection module', () => { expect( state.lastSelectedChannel ).toEqual( 2 ); }); - it('should be able to expand and shrink its selection when starting key selection to the left', () => { + it("should be able to expand and shrink its selection when starting key selection to the left", () => { let keyCode = 37; // left let selectedChannel = 2; const selectedStep = 1;