mirror of
https://github.com/igorski/bitmappery.git
synced 2026-06-17 03:34:56 +02:00
Split menu into separate reusable sections, introduced context menu actions to layer panel. Renamed tool-module to editor-module
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
*/
|
||||
<template>
|
||||
<div id="app" ref="app">
|
||||
<application-menu />
|
||||
<header-menu />
|
||||
<section class="main">
|
||||
<toolbox
|
||||
ref="toolbox"
|
||||
@@ -75,7 +75,7 @@
|
||||
import { type Component, defineAsyncComponent } from "vue";
|
||||
import { mapState, mapGetters, mapMutations, mapActions } from "vuex";
|
||||
import { createI18n } from "vue-i18n";
|
||||
import ApplicationMenu from "@/components/application-menu/application-menu.vue";
|
||||
import HeaderMenu from "@/components/menus/header-menu/header-menu.vue";
|
||||
import ToolOptionsPanel from "@/components/tool-options-panel/tool-options-panel.vue";
|
||||
import LayerPanel from "@/components/layer-panel/layer-panel.vue";
|
||||
import Toolbox from "@/components/toolbox/toolbox.vue";
|
||||
@@ -127,7 +127,7 @@ function asyncComponent( key: string, importFn: () => Promise<any> ): IAsyncComp
|
||||
export default {
|
||||
mixins: [ ImageToDocumentManager ],
|
||||
components: {
|
||||
ApplicationMenu,
|
||||
HeaderMenu,
|
||||
DialogWindow,
|
||||
Loader,
|
||||
Notifications,
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
:class="{
|
||||
'layer--active': element.index === activeLayerIndex
|
||||
}"
|
||||
@contextmenu.stop.prevent="showContextMenu( $event, element )"
|
||||
>
|
||||
<!-- layer name is an input on double click -->
|
||||
<input
|
||||
@@ -143,6 +144,14 @@
|
||||
@click="requestMaskAdd()"
|
||||
></button>
|
||||
</div>
|
||||
<context-menu
|
||||
v-if="contextMenu.show"
|
||||
@close="contextMenu.show = false"
|
||||
:x="contextMenu.x"
|
||||
:y="contextMenu.y"
|
||||
>
|
||||
<layer-menu :opened="true" />
|
||||
</context-menu>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -173,12 +182,19 @@ type IndexedLayer = Layer & { index: number, maskSelected: boolean };
|
||||
export default {
|
||||
i18n: { messages },
|
||||
components: {
|
||||
ContextMenu : defineAsyncComponent({ loader: () => import( "@/components/menus/context-menu/context-menu.vue" ) }),
|
||||
Draggable : defineAsyncComponent({ loader: () => import( "vuedraggable" ) }),
|
||||
LayerEffects : defineAsyncComponent({ loader: () => import( "@/components/layer-effects/layer-effects.vue" ) }),
|
||||
LayerMenu : defineAsyncComponent({ loader: () => import( "@/components/menus/layer-menu/layer-menu.vue" ) }),
|
||||
},
|
||||
data: () => ({
|
||||
editable: false,
|
||||
showEffects: false,
|
||||
contextMenu: {
|
||||
show: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
}),
|
||||
computed: {
|
||||
...mapState([
|
||||
@@ -359,6 +375,12 @@ export default {
|
||||
this.$refs.nameInput?.select();
|
||||
}
|
||||
},
|
||||
showContextMenu( event: PointerEvent, layer: IndexedLayer ): void {
|
||||
this.handleLayerClick( layer );
|
||||
this.contextMenu.show = true;
|
||||
this.contextMenu.x = event.clientX;
|
||||
this.contextMenu.y = event.clientY;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
96
src/components/menus/context-menu/context-menu.vue
Normal file
96
src/components/menus/context-menu/context-menu.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Igor Zinken 2025 - 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
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
<template>
|
||||
<div
|
||||
class="context-menu menu-list"
|
||||
:style="position"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
const MENU_WIDTH = 300; // see style
|
||||
const MENU_HEIGHT = 205;
|
||||
|
||||
export default {
|
||||
emits: [ "close" ],
|
||||
props: {
|
||||
x: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
y: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
position(): { left: string, top: string } {
|
||||
return {
|
||||
left : `${this.x - ( MENU_WIDTH / 2 )}px`,
|
||||
top : `${this.y - ( MENU_HEIGHT / 2 )}px`
|
||||
};
|
||||
},
|
||||
},
|
||||
mounted(): void {
|
||||
this.closeHandler = this.handleClose.bind( this );
|
||||
|
||||
document.addEventListener( "click", this.closeHandler );
|
||||
document.addEventListener( "contextmenu", this.closeHandler );
|
||||
},
|
||||
beforeUnmount(): void {
|
||||
document.removeEventListener( "click", this.closeHandler );
|
||||
document.removeEventListener( "contextmenu", this.closeHandler );
|
||||
},
|
||||
methods: {
|
||||
handleClose(): void {
|
||||
this.$emit( "close" );
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "@/styles/_mixins";
|
||||
@use "@/styles/colors";
|
||||
@use "@/styles/ui";
|
||||
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
background-image: colors.$color-window-bg;
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
|
||||
min-width: 300px;
|
||||
|
||||
@include ui.nestedMenu();
|
||||
}
|
||||
|
||||
.context-menu div {
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -232,68 +232,10 @@
|
||||
<!-- layer menu -->
|
||||
<li>
|
||||
<a v-t="'layer'" class="title" @click.prevent="openSubMenu('layer')"></a>
|
||||
<ul class="submenu"
|
||||
:class="{ opened: activeSubMenu === 'layer' }"
|
||||
<layer-menu
|
||||
:opened="activeSubMenu === 'layer'"
|
||||
@click="close()"
|
||||
>
|
||||
<li>
|
||||
<button
|
||||
v-t="'duplicateLayer'"
|
||||
type="button"
|
||||
:disabled="!activeLayer"
|
||||
@click="requestDuplicateLayer()"
|
||||
></button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
v-t="'commitEffects'"
|
||||
type="button"
|
||||
:disabled="!activeLayerCanBeCommitted"
|
||||
@click="commitLayerEffects()"
|
||||
></button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
v-t="'copyLayerFilters'"
|
||||
type="button"
|
||||
:disabled="!activeLayer"
|
||||
@click="copyLayerFilters()"
|
||||
></button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
v-t="'pasteLayerFilters'"
|
||||
type="button"
|
||||
:disabled="!activeLayer || !clonedFilters"
|
||||
@click="requestPasteLayerFilters()"
|
||||
></button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
v-t="activeLayerHasFiltersEnabled ? 'disableLayerFilters' : 'enableLayerFilters'"
|
||||
v-tooltip.right="$t('toggleLayerFiltersTooltip')"
|
||||
type="button"
|
||||
:disabled="!activeLayer"
|
||||
@click="requestToggleLayerFilters()"
|
||||
></button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
v-t="'mergeDown'"
|
||||
type="button"
|
||||
:disabled="!activeLayer || activeLayerIndex === 0"
|
||||
@click="requestMergeLayerDown()"
|
||||
></button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
v-t="'flattenImage'"
|
||||
type="button"
|
||||
:disabled="!activeLayer || activeDocument.layers.length < 2"
|
||||
@click="requestMergeLayerDown( true )"
|
||||
></button>
|
||||
</li>
|
||||
</ul>
|
||||
/>
|
||||
</li>
|
||||
<!-- selection menu -->
|
||||
<li>
|
||||
@@ -424,8 +366,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent } from "vue";
|
||||
import { mapState, mapGetters, mapMutations, mapActions } from "vuex";
|
||||
import { LayerTypes } from "@/definitions/layer-types";
|
||||
import { MAX_SPRITESHEET_WIDTH } from "@/definitions/editor-properties";
|
||||
import {
|
||||
CREATE_DOCUMENT, RESIZE_DOCUMENT, SAVE_DOCUMENT, EXPORT_WINDOW, LOAD_SELECTION, SAVE_SELECTION,
|
||||
@@ -433,26 +375,21 @@ import {
|
||||
} from "@/definitions/modal-windows";
|
||||
import CloudServiceConnector from "@/mixins/cloud-service-connector";
|
||||
import ImageToDocumentManager from "@/mixins/image-to-document-manager";
|
||||
import { hasFilters } from "@/factories/filters-factory";
|
||||
import { getCanvasInstance } from "@/services/canvas-service";
|
||||
import { cropToSelection } from "@/store/actions/crop-to-selection";
|
||||
import { commitLayerEffectsAndTransforms } from "@/store/actions/layer-commit-effects-and-transforms";
|
||||
import { duplicateLayer } from "@/store/actions/layer-duplicate";
|
||||
import { mergeLayerDown } from "@/store/actions/layer-merge-down";
|
||||
import { pasteLayerFilters } from "@/store/actions/layer-paste-filters";
|
||||
import { toggleLayerFilters } from "@/store/actions/layer-toggle-filters";
|
||||
import { supportsFullscreen, setToggleButton } from "@/utils/environment-util";
|
||||
import { supportsDropbox, supportsGoogleDrive, supportsS3 } from "@/utils/cloud-service-loader";
|
||||
import { hasTransform } from "@/utils/layer-util";
|
||||
import sharedMessages from "@/messages.json"; // for CloudServiceConnector
|
||||
import messages from "./messages.json";
|
||||
|
||||
export default {
|
||||
i18n: { messages, sharedMessages },
|
||||
mixins: [ CloudServiceConnector, ImageToDocumentManager ],
|
||||
components: {
|
||||
LayerMenu : defineAsyncComponent({ loader: () => import( "@/components/menus/layer-menu/layer-menu.vue" ) }),
|
||||
},
|
||||
data: () => ({
|
||||
activeSubMenu: null, // used for mobile views collapsed / expanded view
|
||||
clonedFilters: null,
|
||||
isFullscreen: false,
|
||||
hasDropbox: supportsDropbox(),
|
||||
hasDrive: supportsGoogleDrive(),
|
||||
@@ -487,12 +424,6 @@ export default {
|
||||
hasClipboard(): boolean {
|
||||
return !!this.selectionContent;
|
||||
},
|
||||
activeLayerHasFiltersEnabled(): boolean {
|
||||
return this.activeLayer?.filters?.enabled;
|
||||
},
|
||||
activeLayerCanBeCommitted(): boolean {
|
||||
return !!this.activeLayer && ( hasTransform( this.activeLayer ) || hasFilters( this.activeLayer.filters )) && this.activeLayer.type !== LayerTypes.LAYER_TEXT;
|
||||
},
|
||||
canSnapAndAlign: {
|
||||
get(): boolean {
|
||||
return this.snapAlign;
|
||||
@@ -554,7 +485,6 @@ export default {
|
||||
methods: {
|
||||
...mapMutations([
|
||||
"setMenuOpened",
|
||||
"showNotification",
|
||||
"openModal",
|
||||
"setActiveDocument",
|
||||
"setActiveDocumentSize",
|
||||
@@ -619,25 +549,6 @@ export default {
|
||||
navigateHistory( action = "undo" ): void {
|
||||
this.$store.dispatch( action );
|
||||
},
|
||||
requestDuplicateLayer(): void {
|
||||
duplicateLayer( this.$store, this.activeLayer, this.activeLayerIndex + 1 );
|
||||
},
|
||||
commitLayerEffects(): void {
|
||||
commitLayerEffectsAndTransforms( this.$store, this.activeDocument, this.activeLayer, this.activeLayerIndex );
|
||||
},
|
||||
async requestMergeLayerDown( allLayers = false ): Promise<void> {
|
||||
await mergeLayerDown( this.$store, this.activeDocument, this.activeLayer, this.activeLayerIndex, this.$t( "mergedLayer" ), allLayers );
|
||||
},
|
||||
copyLayerFilters(): void {
|
||||
this.clonedFilters = { ...this.activeLayer.filters };
|
||||
this.showNotification({ message: this.$t( "filtersCopied" ) });
|
||||
},
|
||||
requestPasteLayerFilters(): void {
|
||||
pasteLayerFilters( this.$store, this.clonedFilters, this.activeLayer, this.activeLayerIndex );
|
||||
},
|
||||
requestToggleLayerFilters(): void {
|
||||
toggleLayerFilters( this.$store, this.activeLayer, this.activeLayerIndex );
|
||||
},
|
||||
selectAll(): void {
|
||||
getCanvasInstance()?.interactionPane.selectAll();
|
||||
},
|
||||
@@ -656,6 +567,7 @@ export default {
|
||||
@use "@/styles/_mixins";
|
||||
@use "@/styles/_variables";
|
||||
@use "@/styles/typography";
|
||||
@use "@/styles/ui";
|
||||
|
||||
$toggle-width: 50px;
|
||||
|
||||
@@ -679,6 +591,7 @@ $toggle-width: 50px;
|
||||
height: variables.$menu-height;
|
||||
background-image: colors.$color-window-bg;
|
||||
@include mixins.boxSize();
|
||||
@include ui.nestedMenu();
|
||||
|
||||
@include mixins.large() {
|
||||
min-width: 100%;
|
||||
@@ -798,125 +711,6 @@ h1 {
|
||||
}
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
flex: 1;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@include mixins.boxSize();
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
padding: 0 variables.$spacing-medium 0 0;
|
||||
margin: 0;
|
||||
font-family: Montserrat, Helvetica, Verdana;
|
||||
cursor: pointer;
|
||||
|
||||
a {
|
||||
color: #b6b6b6;
|
||||
text-decoration: none;
|
||||
padding-bottom: variables.$spacing-large;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:hover a {
|
||||
color: colors.$color-1;
|
||||
border-bottom: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
a {
|
||||
border-bottom: 3px solid #555;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
color: #b6b6b6;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 95%;
|
||||
|
||||
&:disabled {
|
||||
color: colors.$color-bg !important;
|
||||
}
|
||||
|
||||
&.checked::before {
|
||||
content: "\2713";
|
||||
margin-right: variables.$spacing-small;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: colors.$color-4;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@include mixins.large() {
|
||||
&:hover, &:focus {
|
||||
a {
|
||||
color: colors.$color-1;
|
||||
}
|
||||
ul {
|
||||
display: block;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
display: none;
|
||||
position: absolute;
|
||||
box-shadow: 0 0 5px rgba(0,0,0,.5);
|
||||
padding: variables.$spacing-medium;
|
||||
background-image: colors.$color-window-bg;
|
||||
background-repeat: repeat-x;
|
||||
@include mixins.boxSize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.mobile() {
|
||||
position: absolute;
|
||||
top: variables.$menu-height;
|
||||
background-image: linear-gradient(to bottom,#fff 35%,#eee 90%);
|
||||
background-repeat: repeat-x;
|
||||
display: none;
|
||||
|
||||
.title {
|
||||
padding: variables.$spacing-small variables.$spacing-medium;
|
||||
}
|
||||
|
||||
.submenu {
|
||||
display: none;
|
||||
|
||||
&.opened {
|
||||
display: block;
|
||||
padding-left: variables.$spacing-medium;
|
||||
background-image: colors.$color-window-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submenu {
|
||||
@include mixins.large() {
|
||||
li {
|
||||
display: block;
|
||||
color: #b6b6b6;
|
||||
padding: variables.$spacing-xsmall variables.$spacing-medium;
|
||||
|
||||
&:hover {
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-selector {
|
||||
display: none;
|
||||
}
|
||||
@@ -25,17 +25,6 @@
|
||||
"canvasSize": "Canvas size",
|
||||
"sliceGridToLayers": "Slice grid to layers",
|
||||
"layer": "Layer",
|
||||
"duplicateLayer": "Duplicate layer",
|
||||
"commitEffects": "Commit effects and transforms",
|
||||
"mergeDown": "Merge down",
|
||||
"flattenImage": "Flatten image",
|
||||
"mergedLayer": "Merged layer",
|
||||
"copyLayerFilters": "Copy layer filters",
|
||||
"pasteLayerFilters": "Paste copied filters",
|
||||
"filtersCopied": "Filters copied",
|
||||
"disableLayerFilters": "Disable layer filters",
|
||||
"enableLayerFilters": "Enable layer filters",
|
||||
"toggleLayerFiltersTooltip": "(alt + F)",
|
||||
"selection": "Selection",
|
||||
"deselectAll": "Deselect all",
|
||||
"deselectAllTooltip": "(alt + D)",
|
||||
155
src/components/menus/layer-menu/layer-menu.vue
Normal file
155
src/components/menus/layer-menu/layer-menu.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Igor Zinken 2020-2025 - 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
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
<template>
|
||||
<ul
|
||||
class="submenu"
|
||||
:class="{ opened: opened }"
|
||||
>
|
||||
<li>
|
||||
<button
|
||||
v-t="'duplicateLayer'"
|
||||
type="button"
|
||||
:disabled="!activeLayer"
|
||||
@click="requestDuplicateLayer()"
|
||||
></button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
v-t="'commitEffects'"
|
||||
type="button"
|
||||
:disabled="!activeLayerCanBeCommitted"
|
||||
@click="commitLayerEffects()"
|
||||
></button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
v-t="'copyLayerFilters'"
|
||||
type="button"
|
||||
:disabled="!activeLayer"
|
||||
@click="copyLayerFilters()"
|
||||
></button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
v-t="'pasteLayerFilters'"
|
||||
type="button"
|
||||
:disabled="!activeLayer || !clonedFilters"
|
||||
@click="requestPasteLayerFilters()"
|
||||
></button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
v-t="activeLayerHasFiltersEnabled ? 'disableLayerFilters' : 'enableLayerFilters'"
|
||||
v-tooltip.right="$t('toggleLayerFiltersTooltip')"
|
||||
type="button"
|
||||
:disabled="!activeLayer"
|
||||
@click="requestToggleLayerFilters()"
|
||||
></button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
v-t="'mergeDown'"
|
||||
type="button"
|
||||
:disabled="!activeLayer || activeLayerIndex === 0"
|
||||
@click="requestMergeLayerDown()"
|
||||
></button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
v-t="'flattenImage'"
|
||||
type="button"
|
||||
:disabled="!activeLayer || activeDocument.layers.length < 2"
|
||||
@click="requestMergeLayerDown( true )"
|
||||
></button>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { mapGetters, mapMutations } from "vuex";
|
||||
import { LayerTypes } from "@/definitions/layer-types";
|
||||
import { hasFilters } from "@/factories/filters-factory";
|
||||
import { commitLayerEffectsAndTransforms } from "@/store/actions/layer-commit-effects-and-transforms";
|
||||
import { duplicateLayer } from "@/store/actions/layer-duplicate";
|
||||
import { mergeLayerDown } from "@/store/actions/layer-merge-down";
|
||||
import { pasteLayerFilters } from "@/store/actions/layer-paste-filters";
|
||||
import { toggleLayerFilters } from "@/store/actions/layer-toggle-filters";
|
||||
import { hasTransform } from "@/utils/layer-util";
|
||||
|
||||
import messages from "./messages.json";
|
||||
|
||||
export default {
|
||||
i18n: { messages },
|
||||
props: {
|
||||
opened: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
"activeDocument",
|
||||
"activeLayer",
|
||||
"activeLayerIndex",
|
||||
"clonedFilters",
|
||||
]),
|
||||
activeLayerCanBeCommitted(): boolean {
|
||||
return !!this.activeLayer && ( hasTransform( this.activeLayer ) || hasFilters( this.activeLayer.filters )) && this.activeLayer.type !== LayerTypes.LAYER_TEXT;
|
||||
},
|
||||
activeLayerHasFiltersEnabled(): boolean {
|
||||
return this.activeLayer?.filters?.enabled;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapMutations([
|
||||
"setClonedFilters",
|
||||
"showNotification",
|
||||
]),
|
||||
requestDuplicateLayer(): void {
|
||||
duplicateLayer( this.$store, this.activeLayer, this.activeLayerIndex + 1 );
|
||||
},
|
||||
commitLayerEffects(): void {
|
||||
commitLayerEffectsAndTransforms( this.$store, this.activeDocument, this.activeLayer, this.activeLayerIndex );
|
||||
},
|
||||
async requestMergeLayerDown( allLayers = false ): Promise<void> {
|
||||
await mergeLayerDown( this.$store, this.activeDocument, this.activeLayer, this.activeLayerIndex, this.$t( "mergedLayer" ), allLayers );
|
||||
},
|
||||
copyLayerFilters(): void {
|
||||
this.setClonedFilters({ ...this.activeLayer.filters });
|
||||
this.showNotification({ message: this.$t( "filtersCopied" ) });
|
||||
},
|
||||
requestPasteLayerFilters(): void {
|
||||
pasteLayerFilters( this.$store, this.clonedFilters, this.activeLayer, this.activeLayerIndex );
|
||||
},
|
||||
requestToggleLayerFilters(): void {
|
||||
toggleLayerFilters( this.$store, this.activeLayer, this.activeLayerIndex );
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use "@/styles/ui";
|
||||
|
||||
@include ui.nestedMenu();
|
||||
</style>
|
||||
15
src/components/menus/layer-menu/messages.json
Normal file
15
src/components/menus/layer-menu/messages.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"en-US": {
|
||||
"duplicateLayer": "Duplicate layer",
|
||||
"commitEffects": "Commit effects and transforms",
|
||||
"copyLayerFilters": "Copy layer filters",
|
||||
"pasteLayerFilters": "Paste copied filters",
|
||||
"disableLayerFilters": "Disable layer filters",
|
||||
"enableLayerFilters": "Enable layer filters",
|
||||
"toggleLayerFiltersTooltip": "(alt + F)",
|
||||
"mergeDown": "Merge down",
|
||||
"flattenImage": "Flatten image",
|
||||
"mergedLayer": "Merged layer",
|
||||
"filtersCopied": "Filters copied"
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ const BrushFactory = {
|
||||
radius,
|
||||
colors,
|
||||
pointers,
|
||||
options, // provided by tool-module
|
||||
options, // provided by editor-module
|
||||
halfRadius : radius * 0.5,
|
||||
doubleRadius : radius * 2,
|
||||
down : false
|
||||
|
||||
@@ -39,12 +39,12 @@ import { copySelection, deleteSelectionContent } from "@/utils/document-util";
|
||||
import { saveBlobAsFile, selectFile } from "@/utils/file-util";
|
||||
import { replaceLayerSource } from "@/utils/layer-util";
|
||||
import { truncate } from "@/utils/string-util";
|
||||
import canvas, { CanvasState } from "./modules/canvas-module";
|
||||
import document, { DocumentState } from "./modules/document-module";
|
||||
import history, { HistoryState } from "./modules/history-module";
|
||||
import image, { ImageState } from "./modules/image-module";
|
||||
import preferences, { PreferencesState } from "./modules/preferences-module";
|
||||
import tool, { ToolState } from "./modules/tool-module";
|
||||
import canvas, { type CanvasState } from "./modules/canvas-module";
|
||||
import document, { type DocumentState } from "./modules/document-module";
|
||||
import history, { type HistoryState } from "./modules/history-module";
|
||||
import image, { type ImageState } from "./modules/image-module";
|
||||
import preferences, { type PreferencesState } from "./modules/preferences-module";
|
||||
import editor, { type EditorState } from "./modules/editor-module";
|
||||
|
||||
export interface BitMapperyState {
|
||||
menuOpened: boolean;
|
||||
@@ -72,7 +72,7 @@ export interface BitMapperyState {
|
||||
history: HistoryState;
|
||||
image: ImageState;
|
||||
preferences: PreferencesState;
|
||||
tool: ToolState;
|
||||
editor: EditorState;
|
||||
};
|
||||
|
||||
// cheat a little by exposing the vue-i18n translations directly to the
|
||||
@@ -90,7 +90,7 @@ export default {
|
||||
history,
|
||||
image,
|
||||
preferences,
|
||||
tool,
|
||||
editor,
|
||||
},
|
||||
// @ts-expect-error sub module states are injected by Vuex on store creation
|
||||
state: (): BitMapperyState => ({
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import type { Module } from "vuex";
|
||||
import type { Document } from "@/definitions/document";
|
||||
import type { Document, Filters } from "@/definitions/document";
|
||||
import type {
|
||||
ZoomToolOptions, BrushToolOptions, EraserToolOptions, CloneToolOptions,
|
||||
SelectionToolOptions, FillToolOptions, WandToolOptions
|
||||
@@ -30,7 +30,7 @@ import ToolTypes, { TOOL_SRC_MERGED } from "@/definitions/tool-types";
|
||||
import BrushTypes from "@/definitions/brush-types";
|
||||
import { runRendererFn } from "@/factories/renderer-factory";
|
||||
|
||||
export interface ToolState {
|
||||
export interface EditorState {
|
||||
activeTool: ToolTypes;
|
||||
activeColor: string;
|
||||
options: {
|
||||
@@ -45,9 +45,10 @@ export interface ToolState {
|
||||
snapAlign: boolean;
|
||||
antiAlias: boolean;
|
||||
pixelGrid: boolean;
|
||||
clonedFilters: Filters | null;
|
||||
};
|
||||
|
||||
export const createToolState = ( props?: Partial<ToolState> ): ToolState => ({
|
||||
export const createEditorState = ( props?: Partial<EditorState> ): EditorState => ({
|
||||
activeTool : null,
|
||||
activeColor : "rgba(255,0,0,1)",
|
||||
options : {
|
||||
@@ -62,40 +63,42 @@ export const createToolState = ( props?: Partial<ToolState> ): ToolState => ({
|
||||
snapAlign : true,
|
||||
antiAlias : true,
|
||||
pixelGrid : false,
|
||||
clonedFilters: null,
|
||||
...props,
|
||||
});
|
||||
|
||||
const ToolModule: Module<ToolState, any> = {
|
||||
state: (): ToolState => createToolState(),
|
||||
const EditorModule: Module<EditorState, any> = {
|
||||
state: (): EditorState => createEditorState(),
|
||||
getters: {
|
||||
activeTool : ( state: ToolState ): ToolTypes => state.activeTool,
|
||||
activeColor : ( state: ToolState ): string => state.activeColor,
|
||||
activeTool : ( state: EditorState ): ToolTypes => state.activeTool,
|
||||
activeColor : ( state: EditorState ): string => state.activeColor,
|
||||
clonedFilters : ( state: EditorState ): Filters => state.clonedFilters,
|
||||
// @ts-expect-error Element implicitly has an 'any' type because expression of type 'ToolTypes' can't be used to index type
|
||||
activeToolOptions : ( state: ToolState ): any => state.options[ state.activeTool ],
|
||||
selectionOptions : ( state: ToolState ): SelectionToolOptions => state.options[ ToolTypes.SELECTION ],
|
||||
zoomOptions : ( state: ToolState ): ZoomToolOptions => state.options[ ToolTypes.ZOOM ],
|
||||
brushOptions : ( state: ToolState ): BrushToolOptions => state.options[ ToolTypes.BRUSH ],
|
||||
eraserOptions : ( state: ToolState ): EraserToolOptions => state.options[ ToolTypes.ERASER ],
|
||||
cloneOptions : ( state: ToolState ): CloneToolOptions => state.options[ ToolTypes.CLONE ],
|
||||
fillOptions : ( state: ToolState ): FillToolOptions => state.options[ ToolTypes.FILL ],
|
||||
wandOptions : ( state: ToolState ): WandToolOptions => state.options[ ToolTypes.WAND ],
|
||||
snapAlign : ( state: ToolState ): boolean => state.snapAlign,
|
||||
antiAlias : ( state: ToolState ): boolean => state.antiAlias,
|
||||
pixelGrid : ( state: ToolState ): boolean => state.pixelGrid,
|
||||
activeToolOptions : ( state: EditorState ): any => state.options[ state.activeTool ],
|
||||
selectionOptions : ( state: EditorState ): SelectionToolOptions => state.options[ ToolTypes.SELECTION ],
|
||||
zoomOptions : ( state: EditorState ): ZoomToolOptions => state.options[ ToolTypes.ZOOM ],
|
||||
brushOptions : ( state: EditorState ): BrushToolOptions => state.options[ ToolTypes.BRUSH ],
|
||||
eraserOptions : ( state: EditorState ): EraserToolOptions => state.options[ ToolTypes.ERASER ],
|
||||
cloneOptions : ( state: EditorState ): CloneToolOptions => state.options[ ToolTypes.CLONE ],
|
||||
fillOptions : ( state: EditorState ): FillToolOptions => state.options[ ToolTypes.FILL ],
|
||||
wandOptions : ( state: EditorState ): WandToolOptions => state.options[ ToolTypes.WAND ],
|
||||
snapAlign : ( state: EditorState ): boolean => state.snapAlign,
|
||||
antiAlias : ( state: EditorState ): boolean => state.antiAlias,
|
||||
pixelGrid : ( state: EditorState ): boolean => state.pixelGrid,
|
||||
},
|
||||
mutations: {
|
||||
setActiveTool( state: ToolState, { tool, document }: { tool: ToolTypes, document: Document }): void {
|
||||
setActiveTool( state: EditorState, { tool, document }: { tool: ToolTypes, document: Document }): void {
|
||||
state.activeTool = tool;
|
||||
runRendererFn( renderer => {
|
||||
// @ts-expect-error Element implicitly has an 'any' type because expression of type 'ToolTypes' can't be used to index type
|
||||
renderer.handleActiveTool( tool, state.options[ state.activeTool ] as any, document );
|
||||
});
|
||||
},
|
||||
setActiveColor( state: ToolState, color: string ): void {
|
||||
setActiveColor( state: EditorState, color: string ): void {
|
||||
state.activeColor = color;
|
||||
updateLayerRenderers( state.activeColor, state.options[ ToolTypes.BRUSH ] as BrushToolOptions );
|
||||
},
|
||||
setToolOptionValue( state: ToolState, { tool, option, value }: { tool: ToolTypes, option: string, value: any }): void {
|
||||
setToolOptionValue( state: EditorState, { tool, option, value }: { tool: ToolTypes, option: string, value: any }): void {
|
||||
// @ts-expect-error Element implicitly has an 'any' type because expression of type 'ToolTypes' can't be used to index type
|
||||
const toolOptions: any = state.options[ tool ];
|
||||
toolOptions[ option ] = value;
|
||||
@@ -111,18 +114,21 @@ const ToolModule: Module<ToolState, any> = {
|
||||
break;
|
||||
}
|
||||
},
|
||||
setSnapAlign( state: ToolState, enabled: boolean ): void {
|
||||
setSnapAlign( state: EditorState, enabled: boolean ): void {
|
||||
state.snapAlign = enabled;
|
||||
},
|
||||
setAntiAlias( state: ToolState, enabled: boolean ): void {
|
||||
setAntiAlias( state: EditorState, enabled: boolean ): void {
|
||||
state.antiAlias = enabled;
|
||||
},
|
||||
setPixelGrid( state: ToolState, enabled: boolean ): void {
|
||||
setPixelGrid( state: EditorState, enabled: boolean ): void {
|
||||
state.pixelGrid = enabled;
|
||||
},
|
||||
setClonedFilters( state: EditorState, filters: Filters | null ): void {
|
||||
state.clonedFilters = filters;
|
||||
},
|
||||
},
|
||||
};
|
||||
export default ToolModule;
|
||||
export default EditorModule;
|
||||
|
||||
/* internal methods */
|
||||
|
||||
@@ -170,3 +170,124 @@
|
||||
margin: variables.$spacing-small;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin nestedMenu() {
|
||||
.menu-list {
|
||||
flex: 1;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@include mixins.boxSize();
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
padding: 0 variables.$spacing-medium 0 0;
|
||||
margin: 0;
|
||||
font-family: Montserrat, Helvetica, Verdana;
|
||||
cursor: pointer;
|
||||
|
||||
a {
|
||||
color: #b6b6b6;
|
||||
text-decoration: none;
|
||||
padding-bottom: variables.$spacing-large;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:hover a {
|
||||
color: colors.$color-1;
|
||||
border-bottom: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
a {
|
||||
border-bottom: 3px solid #555;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
color: #b6b6b6;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 95%;
|
||||
|
||||
&:disabled {
|
||||
color: colors.$color-bg !important;
|
||||
}
|
||||
|
||||
&.checked::before {
|
||||
content: "\2713";
|
||||
margin-right: variables.$spacing-small;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: colors.$color-4;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@include mixins.large() {
|
||||
&:hover, &:focus {
|
||||
a {
|
||||
color: colors.$color-1;
|
||||
}
|
||||
ul {
|
||||
display: block;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
display: none;
|
||||
position: absolute;
|
||||
box-shadow: 0 0 5px rgba(0,0,0,.5);
|
||||
padding: variables.$spacing-medium;
|
||||
background-image: colors.$color-window-bg;
|
||||
background-repeat: repeat-x;
|
||||
@include mixins.boxSize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.mobile() {
|
||||
position: absolute;
|
||||
top: variables.$menu-height;
|
||||
background-image: linear-gradient(to bottom,#fff 35%,#eee 90%);
|
||||
background-repeat: repeat-x;
|
||||
display: none;
|
||||
|
||||
.title {
|
||||
padding: variables.$spacing-small variables.$spacing-medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submenu {
|
||||
@include mixins.large() {
|
||||
li {
|
||||
display: block;
|
||||
color: #b6b6b6;
|
||||
padding: variables.$spacing-xsmall variables.$spacing-small;
|
||||
|
||||
&:hover {
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.mobile() {
|
||||
display: none;
|
||||
|
||||
&.opened {
|
||||
display: block;
|
||||
padding-left: variables.$spacing-medium;
|
||||
background-image: colors.$color-window-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { createDocumentState } from "@/store/modules/document-module";
|
||||
import { createHistoryState } from "@/store/modules/history-module";
|
||||
import { createImageState } from "@/store/modules/image-module";
|
||||
import { createPreferencesState } from "@/store/modules/preferences-module";
|
||||
import { createToolState } from "@/store/modules/tool-module";
|
||||
import { createEditorState } from "@/store/modules/editor-module";
|
||||
|
||||
// zCanvas mocks
|
||||
// @todo should not be necessary when updating to zCanvas 6+
|
||||
@@ -185,7 +185,7 @@ export function createState( props?: Partial<BitMapperyState> ): BitMapperyState
|
||||
history: createHistoryState(),
|
||||
image: createImageState(),
|
||||
preferences: createPreferencesState(),
|
||||
tool: createToolState(),
|
||||
editor: createEditorState(),
|
||||
...props,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { it, describe, expect } from "vitest";
|
||||
import { mockZCanvas } from "../../mocks";
|
||||
import storeModule, { createToolState } from "@/store/modules/tool-module";
|
||||
import BrushTypes from "@/definitions/brush-types";
|
||||
import ToolTypes, { TOOL_SRC_MERGED } from "@/definitions/tool-types";
|
||||
import FiltersFactory from "@/factories/filters-factory";
|
||||
import storeModule, { createEditorState } from "@/store/modules/editor-module";
|
||||
|
||||
const { getters, mutations } = storeModule;
|
||||
|
||||
mockZCanvas();
|
||||
|
||||
describe( "Vuex tool module", () => {
|
||||
describe( "Vuex editor module", () => {
|
||||
const BASE_OPTIONS = {
|
||||
[ ToolTypes.ZOOM ] : { level: 1 },
|
||||
[ ToolTypes.BRUSH ] : { size: 10, type: BrushTypes.LINE, opacity: 1, strokes: 1, thickness: .5 },
|
||||
@@ -20,13 +21,15 @@ describe( "Vuex tool module", () => {
|
||||
};
|
||||
|
||||
describe( "getters", () => {
|
||||
const state = createToolState({
|
||||
const clonedFilters = FiltersFactory.create();
|
||||
const state = createEditorState({
|
||||
activeTool: ToolTypes.CLONE,
|
||||
activeColor: "red",
|
||||
options: { ...BASE_OPTIONS },
|
||||
snapAlign: true,
|
||||
antiAlias: true,
|
||||
pixelGrid: false,
|
||||
clonedFilters,
|
||||
});
|
||||
|
||||
it( "should be able to return the active tool", () => {
|
||||
@@ -70,21 +73,26 @@ describe( "Vuex tool module", () => {
|
||||
});
|
||||
|
||||
it( "should be able to retrieve the current snap and alignment state", () => {
|
||||
expect( getters.snapAlign ( state, getters, {}, {} )).toBe( true );
|
||||
expect( getters.snapAlign( state, getters, {}, {} )).toBe( true );
|
||||
});
|
||||
|
||||
it( "should be able to retrieve the current anti-aliasing state", () => {
|
||||
expect( getters.antiAlias ( state, getters, {}, {} )).toBe( true );
|
||||
expect( getters.antiAlias( state, getters, {}, {} )).toBe( true );
|
||||
});
|
||||
|
||||
it( "should be able to retrieve the copied filters", () => {
|
||||
expect( getters.clonedFilters( state, getters, {}, {} )).toEqual( clonedFilters );
|
||||
});
|
||||
});
|
||||
|
||||
describe( "mutations", () => {
|
||||
const state = createToolState({
|
||||
const state = createEditorState({
|
||||
activeTool: ToolTypes.ZOOM,
|
||||
activeColor: "red",
|
||||
options: { ...BASE_OPTIONS },
|
||||
snapAlign: true,
|
||||
antiAlias: true,
|
||||
clonedFilters: null,
|
||||
});
|
||||
|
||||
it( "should be able to set the active tool", () => {
|
||||
@@ -114,5 +122,11 @@ describe( "Vuex tool module", () => {
|
||||
mutations.setAntiAlias( state, false );
|
||||
expect( state.antiAlias ).toBe( false );
|
||||
});
|
||||
|
||||
it( "should be able to set the cloned filters", () => {
|
||||
const filters = FiltersFactory.create({ opacity: 0.5 });
|
||||
mutations.setClonedFilters( state, filters );
|
||||
expect( state.clonedFilters ).toEqual( filters );
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user