Add DialogModal and use it for close confirmations and "coming soon" features (#322)
Closes #269 Closes #196 * Add DialogModal and use it for close confirmations and "coming soon" features * Code cleanup; add Enter key to accept emphasized dialog button
This commit is contained in:
parent
1c317d0166
commit
e02250e8c6
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||
<path d="M11.9,9.79L6.96,0.58c-0.41-0.77-1.5-0.77-1.92,0L0.1,9.79C-0.19,10.33,0.2,11,0.81,11h10.39C11.8,11,12.19,10.33,11.9,9.79z M6.9,2.5L6.67,7H5.33L5.1,2.5L6.9,2.5z M6,10.3c-0.63,0-1.15-0.51-1.15-1.15C4.85,8.51,5.37,8,6,8c0.63,0,1.15,0.51,1.15,1.15C7.15,9.78,6.63,10.3,6,10.3z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 353 B |
|
|
@ -216,12 +216,18 @@ img {
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import dialog from "@/utilities/dialog";
|
||||
import document from "@/utilities/document";
|
||||
import fullscreen from "@/utilities/fullscreen";
|
||||
import MainWindow from "@/components/window/MainWindow.vue";
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
|
||||
export default defineComponent({
|
||||
provide: { fullscreen },
|
||||
provide: {
|
||||
dialog,
|
||||
document,
|
||||
fullscreen,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showUnsupportedModal: !("BigInt64Array" in window),
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="right side">
|
||||
<OptionalInput v-model:checked="snappingEnabled" :icon="'Snapping'" title="Snapping" />
|
||||
<OptionalInput v-model:checked="snappingEnabled" @update:checked="comingSoon(200)" :icon="'Snapping'" title="Snapping" />
|
||||
<PopoverButton>
|
||||
<h3>Snapping</h3>
|
||||
<p>More snapping options will be here</p>
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
<Separator :type="SeparatorType.Unrelated" />
|
||||
|
||||
<OptionalInput v-model:checked="gridEnabled" :icon="'Grid'" title="Grid" />
|
||||
<OptionalInput v-model:checked="gridEnabled" @update:checked="comingSoon(318)" :icon="'Grid'" title="Grid" />
|
||||
<PopoverButton>
|
||||
<h3>Grid</h3>
|
||||
<p>More grid options will be here</p>
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<Separator :type="SeparatorType.Unrelated" />
|
||||
|
||||
<OptionalInput v-model:checked="overlaysEnabled" :icon="'Overlays'" title="Overlays" />
|
||||
<OptionalInput v-model:checked="overlaysEnabled" @update:checked="comingSoon(99)" :icon="'Overlays'" title="Overlays" />
|
||||
<PopoverButton>
|
||||
<h3>Overlays</h3>
|
||||
<p>More overlays options will be here</p>
|
||||
|
|
@ -35,9 +35,9 @@
|
|||
<Separator :type="SeparatorType.Unrelated" />
|
||||
|
||||
<RadioInput @update:index="viewModeChanged" v-model:index="viewModeIndex">
|
||||
<IconButton :action="() => _" :icon="'ViewModeNormal'" :size="24" title="View Mode: Normal" />
|
||||
<IconButton :action="() => _" :icon="'ViewModeOutline'" :size="24" title="View Mode: Outline" />
|
||||
<IconButton :action="() => _" :icon="'ViewModePixels'" :size="24" title="View Mode: Pixels" />
|
||||
<IconButton :action="() => {}" :icon="'ViewModeNormal'" :size="24" title="View Mode: Normal" />
|
||||
<IconButton :action="() => comingSoon(319)" :icon="'ViewModeOutline'" :size="24" title="View Mode: Outline" />
|
||||
<IconButton :action="() => comingSoon(320)" :icon="'ViewModePixels'" :size="24" title="View Mode: Pixels" />
|
||||
</RadioInput>
|
||||
<PopoverButton>
|
||||
<h3>View Mode</h3>
|
||||
|
|
@ -72,36 +72,36 @@
|
|||
<LayoutRow :class="'shelf-and-viewport'">
|
||||
<LayoutCol :class="'shelf'">
|
||||
<div class="tools">
|
||||
<ShelfItemInput :icon="'LayoutSelectTool'" title="Select Tool (V)" :active="activeTool === 'Select'" @click="selectTool('Select')" />
|
||||
<ShelfItemInput :icon="'LayoutCropTool'" title="Crop Tool" :active="activeTool === 'Crop'" @click="'tool not implemented' || selectTool('Crop')" />
|
||||
<ShelfItemInput :icon="'LayoutNavigateTool'" title="Navigate Tool (Z)" :active="activeTool === 'Navigate'" @click="'tool not implemented' || selectTool('Navigate')" />
|
||||
<ShelfItemInput :icon="'LayoutEyedropperTool'" title="Eyedropper Tool (I)" :active="activeTool === 'Eyedropper'" @click="selectTool('Eyedropper')" />
|
||||
<ShelfItemInput icon="LayoutSelectTool" title="Select Tool (V)" :active="activeTool === 'Select'" :action="() => selectTool('Select')" />
|
||||
<ShelfItemInput icon="LayoutCropTool" title="Crop Tool" :active="activeTool === 'Crop'" :action="() => comingSoon(289) && selectTool('Crop')" />
|
||||
<ShelfItemInput icon="LayoutNavigateTool" title="Navigate Tool (Z)" :active="activeTool === 'Navigate'" :action="() => comingSoon(155) && selectTool('Navigate')" />
|
||||
<ShelfItemInput icon="LayoutEyedropperTool" title="Eyedropper Tool (I)" :active="activeTool === 'Eyedropper'" :action="() => selectTool('Eyedropper')" />
|
||||
|
||||
<Separator :type="SeparatorType.Section" :direction="SeparatorDirection.Vertical" />
|
||||
|
||||
<ShelfItemInput :icon="'ParametricTextTool'" title="Text Tool (T)" :active="activeTool === 'Text'" @click="'tool not implemented' || selectTool('Text')" />
|
||||
<ShelfItemInput :icon="'ParametricFillTool'" title="Fill Tool (F)" :active="activeTool === 'Fill'" @click="selectTool('Fill')" />
|
||||
<ShelfItemInput :icon="'ParametricGradientTool'" title="Gradient Tool (H)" :active="activeTool === 'Gradient'" @click="'tool not implemented' || selectTool('Gradient')" />
|
||||
<ShelfItemInput icon="ParametricTextTool" title="Text Tool (T)" :active="activeTool === 'Text'" :action="() => comingSoon(153) && selectTool('Text')" />
|
||||
<ShelfItemInput icon="ParametricFillTool" title="Fill Tool (F)" :active="activeTool === 'Fill'" :action="() => selectTool('Fill')" />
|
||||
<ShelfItemInput icon="ParametricGradientTool" title="Gradient Tool (H)" :active="activeTool === 'Gradient'" :action="() => comingSoon() && selectTool('Gradient')" />
|
||||
|
||||
<Separator :type="SeparatorType.Section" :direction="SeparatorDirection.Vertical" />
|
||||
|
||||
<ShelfItemInput :icon="'RasterBrushTool'" title="Brush Tool (B)" :active="activeTool === 'Brush'" @click="'tool not implemented' || selectTool('Brush')" />
|
||||
<ShelfItemInput :icon="'RasterHealTool'" title="Heal Tool (J)" :active="activeTool === 'Heal'" @click="'tool not implemented' || selectTool('Heal')" />
|
||||
<ShelfItemInput :icon="'RasterCloneTool'" title="Clone Tool (C)" :active="activeTool === 'Clone'" @click="'tool not implemented' || selectTool('Clone')" />
|
||||
<ShelfItemInput :icon="'RasterPatchTool'" title="Patch Tool" :active="activeTool === 'Patch'" @click="'tool not implemented' || selectTool('Patch')" />
|
||||
<ShelfItemInput :icon="'RasterBlurSharpenTool'" title="Detail Tool (D)" :active="activeTool === 'BlurSharpen'" @click="'tool not implemented' || selectTool('BlurSharpen')" />
|
||||
<ShelfItemInput :icon="'RasterRelightTool'" title="Relight Tool (O)" :active="activeTool === 'Relight'" @click="'tool not implemented' || selectTool('Relight')" />
|
||||
<ShelfItemInput icon="RasterBrushTool" title="Brush Tool (B)" :active="activeTool === 'Brush'" :action="() => comingSoon() && selectTool('Brush')" />
|
||||
<ShelfItemInput icon="RasterHealTool" title="Heal Tool (J)" :active="activeTool === 'Heal'" :action="() => comingSoon() && selectTool('Heal')" />
|
||||
<ShelfItemInput icon="RasterCloneTool" title="Clone Tool (C)" :active="activeTool === 'Clone'" :action="() => comingSoon() && selectTool('Clone')" />
|
||||
<ShelfItemInput icon="RasterPatchTool" title="Patch Tool" :active="activeTool === 'Patch'" :action="() => comingSoon() && selectTool('Patch')" />
|
||||
<ShelfItemInput icon="RasterBlurSharpenTool" title="Detail Tool (D)" :active="activeTool === 'BlurSharpen'" :action="() => comingSoon() && selectTool('BlurSharpen')" />
|
||||
<ShelfItemInput icon="RasterRelightTool" title="Relight Tool (O)" :active="activeTool === 'Relight'" :action="() => comingSoon() && selectTool('Relight')" />
|
||||
|
||||
<Separator :type="SeparatorType.Section" :direction="SeparatorDirection.Vertical" />
|
||||
|
||||
<ShelfItemInput :icon="'VectorPathTool'" title="Path Tool (A)" :active="activeTool === 'Path'" @click="'tool not implemented' || selectTool('Path')" />
|
||||
<ShelfItemInput :icon="'VectorPenTool'" title="Pen Tool (P)" :active="activeTool === 'Pen'" @click="selectTool('Pen')" />
|
||||
<ShelfItemInput :icon="'VectorFreehandTool'" title="Freehand Tool (N)" :active="activeTool === 'Freehand'" @click="'tool not implemented' || selectTool('Freehand')" />
|
||||
<ShelfItemInput :icon="'VectorSplineTool'" title="Spline Tool" :active="activeTool === 'Spline'" @click="'tool not implemented' || selectTool('Spline')" />
|
||||
<ShelfItemInput :icon="'VectorLineTool'" title="Line Tool (L)" :active="activeTool === 'Line'" @click="selectTool('Line')" />
|
||||
<ShelfItemInput :icon="'VectorRectangleTool'" title="Rectangle Tool (M)" :active="activeTool === 'Rectangle'" @click="selectTool('Rectangle')" />
|
||||
<ShelfItemInput :icon="'VectorEllipseTool'" title="Ellipse Tool (E)" :active="activeTool === 'Ellipse'" @click="selectTool('Ellipse')" />
|
||||
<ShelfItemInput :icon="'VectorShapeTool'" title="Shape Tool (Y)" :active="activeTool === 'Shape'" @click="selectTool('Shape')" />
|
||||
<ShelfItemInput icon="VectorPathTool" title="Path Tool (A)" :active="activeTool === 'Path'" :action="() => comingSoon(82) && selectTool('Path')" />
|
||||
<ShelfItemInput icon="VectorPenTool" title="Pen Tool (P)" :active="activeTool === 'Pen'" :action="() => selectTool('Pen')" />
|
||||
<ShelfItemInput icon="VectorFreehandTool" title="Freehand Tool (N)" :active="activeTool === 'Freehand'" :action="() => comingSoon() && selectTool('Freehand')" />
|
||||
<ShelfItemInput icon="VectorSplineTool" title="Spline Tool" :active="activeTool === 'Spline'" :action="() => comingSoon() && selectTool('Spline')" />
|
||||
<ShelfItemInput icon="VectorLineTool" title="Line Tool (L)" :active="activeTool === 'Line'" :action="() => selectTool('Line')" />
|
||||
<ShelfItemInput icon="VectorRectangleTool" title="Rectangle Tool (M)" :active="activeTool === 'Rectangle'" :action="() => selectTool('Rectangle')" />
|
||||
<ShelfItemInput icon="VectorEllipseTool" title="Ellipse Tool (E)" :active="activeTool === 'Ellipse'" :action="() => selectTool('Ellipse')" />
|
||||
<ShelfItemInput icon="VectorShapeTool" title="Shape Tool (Y)" :active="activeTool === 'Shape'" :action="() => selectTool('Shape')" />
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="working-colors">
|
||||
|
|
@ -216,6 +216,7 @@ import { defineComponent } from "vue";
|
|||
import { makeModifiersBitfield } from "@/utilities/input";
|
||||
import { ResponseType, registerResponseHandler, Response, UpdateCanvas, SetActiveTool, ExportDocument, SetCanvasZoom, SetCanvasRotation } from "@/utilities/response-handler";
|
||||
import { SeparatorDirection, SeparatorType } from "@/components/widgets/widgets";
|
||||
import comingSoon from "@/utilities/coming-soon";
|
||||
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||
|
|
@ -373,6 +374,7 @@ export default defineComponent({
|
|||
ScrollbarDirection,
|
||||
RulerDirection,
|
||||
SeparatorType,
|
||||
comingSoon,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
<template>
|
||||
<div class="dialog-modal">
|
||||
<FloatingMenu :type="MenuType.Dialog" :direction="MenuDirection.Center">
|
||||
<LayoutRow>
|
||||
<LayoutCol :class="'icon-column'">
|
||||
<!-- `dialog.icon` class exists to provide special sizing in CSS to specific icons -->
|
||||
<IconLabel :icon="dialog.icon" :class="dialog.icon.toLowerCase()" />
|
||||
</LayoutCol>
|
||||
<LayoutCol :class="'main-column'">
|
||||
<TextLabel :bold="true" :class="'heading'">{{ dialog.heading }}</TextLabel>
|
||||
<TextLabel :class="'details'">{{ dialog.details }}</TextLabel>
|
||||
<LayoutRow :class="'buttons-row'">
|
||||
<TextButton v-for="(button, index) in dialog.buttons" :key="index" :title="button.tooltip" :action="button.callback" v-bind="button.props" />
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
</LayoutRow>
|
||||
</FloatingMenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.dialog-modal {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.dialog {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.floating-menu-container .floating-menu-content {
|
||||
pointer-events: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.icon-column {
|
||||
margin-right: 24px;
|
||||
|
||||
.icon-label {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
|
||||
&.file,
|
||||
&.copy {
|
||||
width: 60px;
|
||||
|
||||
svg {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 -10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-column {
|
||||
.heading {
|
||||
white-space: pre;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.details {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.buttons-row {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
import { dismissDialog } from "@/utilities/dialog";
|
||||
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||
import FloatingMenu, { MenuDirection, MenuType } from "@/components/widgets/floating-menus/FloatingMenu.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||
import TextButton from "@/components/widgets/buttons/TextButton.vue";
|
||||
|
||||
export default defineComponent({
|
||||
inject: ["dialog"],
|
||||
components: {
|
||||
LayoutRow,
|
||||
LayoutCol,
|
||||
FloatingMenu,
|
||||
IconLabel,
|
||||
TextLabel,
|
||||
TextButton,
|
||||
},
|
||||
methods: {
|
||||
dismiss() {
|
||||
dismissDialog();
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
MenuDirection,
|
||||
MenuType,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="floating-menu" :class="[direction.toLowerCase(), type.toLowerCase()]" v-if="open" ref="floatingMenu">
|
||||
<div class="floating-menu" :class="[direction.toLowerCase(), type.toLowerCase()]" v-if="open || type === MenuType.Dialog" ref="floatingMenu">
|
||||
<div class="tail" v-if="type === MenuType.Popover"></div>
|
||||
<div class="floating-menu-container" ref="floatingMenuContainer">
|
||||
<div class="floating-menu-content" :class="{ 'scrollable-y': scrollable }" ref="floatingMenuContent" :style="floatingMenuContentStyle">
|
||||
|
|
@ -112,6 +112,15 @@
|
|||
--floating-menu-content-border-radius: 4px;
|
||||
}
|
||||
|
||||
&.center {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.floating-menu-content {
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
&.top,
|
||||
&.bottom {
|
||||
flex-direction: column;
|
||||
|
|
@ -179,11 +188,13 @@ export enum MenuDirection {
|
|||
TopRight = "TopRight",
|
||||
BottomLeft = "BottomLeft",
|
||||
BottomRight = "BottomRight",
|
||||
Center = "Center",
|
||||
}
|
||||
|
||||
export enum MenuType {
|
||||
Popover = "Popover",
|
||||
Dropdown = "Dropdown",
|
||||
Dialog = "Dialog",
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<IconLabel :icon="entry.icon" v-if="entry.icon" />
|
||||
<span v-if="entry.label">{{ entry.label }}</span>
|
||||
</div>
|
||||
<MenuList :menuEntries="entry.children" :direction="MenuDirection.Bottom" :minWidth="240" :drawIcon="true" :defaultAction="actionNotImplemented" :ref="(ref) => setEntryRefs(entry, ref)" />
|
||||
<MenuList :menuEntries="entry.children" :direction="MenuDirection.Bottom" :minWidth="240" :drawIcon="true" :defaultAction="comingSoon" :ref="(ref) => setEntryRefs(entry, ref)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -52,6 +52,9 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
import comingSoon from "@/utilities/coming-soon";
|
||||
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
import { ApplicationPlatform } from "@/components/window/MainWindow.vue";
|
||||
import MenuList, { MenuListEntry, MenuListEntries } from "@/components/widgets/floating-menus/MenuList.vue";
|
||||
|
|
@ -138,17 +141,17 @@ const menuEntries: MenuListEntries = [
|
|||
{
|
||||
label: "Document",
|
||||
ref: undefined,
|
||||
children: [[{ label: "Menu not yet populated" }]],
|
||||
children: [[{ label: "Menu entries coming soon" }]],
|
||||
},
|
||||
{
|
||||
label: "View",
|
||||
ref: undefined,
|
||||
children: [[{ label: "Menu not yet populated" }]],
|
||||
children: [[{ label: "Menu entries coming soon" }]],
|
||||
},
|
||||
{
|
||||
label: "Help",
|
||||
ref: undefined,
|
||||
children: [[{ label: "Menu not yet populated" }]],
|
||||
children: [[{ label: "Menu entries coming soon" }]],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -164,16 +167,13 @@ export default defineComponent({
|
|||
handleLogoClick() {
|
||||
window.open("https://www.graphite.design", "_blank");
|
||||
},
|
||||
actionNotImplemented() {
|
||||
// eslint-disable-next-line no-alert
|
||||
alert("This action is not yet implemented");
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ApplicationPlatform,
|
||||
menuEntries,
|
||||
MenuDirection,
|
||||
comingSoon,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="shelf-item-input" :class="{ active: active }">
|
||||
<IconButton :action="() => _" :icon="icon" :size="32" />
|
||||
<IconButton :action="action" :icon="icon" :size="32" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -37,6 +37,7 @@ export default defineComponent({
|
|||
components: { IconButton },
|
||||
props: {
|
||||
icon: { type: String, required: true },
|
||||
action: { type: Function, required: true },
|
||||
active: { type: Boolean, default: false },
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ import Grid from "@/../assets/12px-solid/grid.svg";
|
|||
import Overlays from "@/../assets/12px-solid/overlays.svg";
|
||||
import Snapping from "@/../assets/12px-solid/snapping.svg";
|
||||
import Info from "@/../assets/12px-solid/info.svg";
|
||||
import Warning from "@/../assets/12px-solid/warning.svg";
|
||||
import Swap from "@/../assets/12px-solid/swap.svg";
|
||||
import ResetColors from "@/../assets/12px-solid/reset-colors.svg";
|
||||
import DropdownArrow from "@/../assets/12px-solid/dropdown-arrow.svg";
|
||||
|
|
@ -168,6 +169,7 @@ const icons = {
|
|||
Overlays: { component: Overlays, size: 12 },
|
||||
Snapping: { component: Snapping, size: 12 },
|
||||
Info: { component: Info, size: 12 },
|
||||
Warning: { component: Warning, size: 12 },
|
||||
Swap: { component: Swap, size: 12 },
|
||||
ResetColors: { component: ResetColors, size: 12 },
|
||||
DropdownArrow: { component: DropdownArrow, size: 12 },
|
||||
|
|
|
|||
|
|
@ -2,12 +2,7 @@
|
|||
<div class="tool-options">
|
||||
<template v-for="(option, index) in toolOptions[activeTool] || []" :key="index">
|
||||
<!-- TODO: Use `<component :is="" v-bind="attributesObject"></component>` to avoid all the separate components with `v-if` -->
|
||||
<IconButton
|
||||
v-if="option.kind === 'IconButton'"
|
||||
:action="() => (option.message && sendToolMessage(option.message), option.callback && option.callback())"
|
||||
:title="option.tooltip"
|
||||
v-bind="option.props"
|
||||
/>
|
||||
<IconButton v-if="option.kind === 'IconButton'" :action="() => handleIconButtonAction(option)" :title="option.tooltip" v-bind="option.props" />
|
||||
<PopoverButton v-if="option.kind === 'PopoverButton'" :title="option.tooltip" :action="option.callback" v-bind="option.props">
|
||||
<h3>{{ option.popover.title }}</h3>
|
||||
<p>{{ option.popover.text }}</p>
|
||||
|
|
@ -30,7 +25,9 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
import { WidgetRow, SeparatorType } from "@/components/widgets/widgets";
|
||||
import comingSoon from "@/utilities/coming-soon";
|
||||
|
||||
import { WidgetRow, SeparatorType, IconButtonWidget } from "@/components/widgets/widgets";
|
||||
import Separator from "@/components/widgets/separators/Separator.vue";
|
||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
|
||||
|
|
@ -53,10 +50,23 @@ export default defineComponent({
|
|||
// This is a placeholder call, using the Shape tool as an example
|
||||
set_tool_options(this.$props.activeTool || "", { Shape: { shape_type: { Polygon: { vertices: newValue } } } });
|
||||
},
|
||||
async sendToolMessage(message: string) {
|
||||
async sendToolMessage(message: string | object) {
|
||||
const { send_tool_message } = await wasm;
|
||||
send_tool_message(this.$props.activeTool || "", message);
|
||||
},
|
||||
handleIconButtonAction(option: IconButtonWidget) {
|
||||
if (option.message) {
|
||||
this.sendToolMessage(option.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (option.callback) {
|
||||
option.callback();
|
||||
return;
|
||||
}
|
||||
|
||||
comingSoon();
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const toolOptions: Record<string, WidgetRow> = {
|
||||
|
|
@ -100,11 +110,11 @@ export default defineComponent({
|
|||
|
||||
{ kind: "Separator", props: { type: SeparatorType.Section } },
|
||||
|
||||
{ kind: "IconButton", tooltip: "Boolean Union", props: { icon: "BooleanUnion", size: 24 } },
|
||||
{ kind: "IconButton", tooltip: "Boolean Subtract Front", props: { icon: "BooleanSubtractFront", size: 24 } },
|
||||
{ kind: "IconButton", tooltip: "Boolean Subtract Back", props: { icon: "BooleanSubtractBack", size: 24 } },
|
||||
{ kind: "IconButton", tooltip: "Boolean Intersect", props: { icon: "BooleanIntersect", size: 24 } },
|
||||
{ kind: "IconButton", tooltip: "Boolean Difference", props: { icon: "BooleanDifference", size: 24 } },
|
||||
{ kind: "IconButton", tooltip: "Boolean Union", callback: () => comingSoon(197), props: { icon: "BooleanUnion", size: 24 } },
|
||||
{ kind: "IconButton", tooltip: "Boolean Subtract Front", callback: () => comingSoon(197), props: { icon: "BooleanSubtractFront", size: 24 } },
|
||||
{ kind: "IconButton", tooltip: "Boolean Subtract Back", callback: () => comingSoon(197), props: { icon: "BooleanSubtractBack", size: 24 } },
|
||||
{ kind: "IconButton", tooltip: "Boolean Intersect", callback: () => comingSoon(197), props: { icon: "BooleanIntersect", size: 24 } },
|
||||
{ kind: "IconButton", tooltip: "Boolean Difference", callback: () => comingSoon(197), props: { icon: "BooleanDifference", size: 24 } },
|
||||
|
||||
{ kind: "Separator", props: { type: SeparatorType.Related } },
|
||||
|
||||
|
|
@ -123,6 +133,7 @@ export default defineComponent({
|
|||
return {
|
||||
toolOptions,
|
||||
SeparatorType,
|
||||
comingSoon,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export type Widgets = IconButtonWidget | SeparatorWidget | PopoverButtonWidget | NumberInputWidget;
|
||||
export type Widgets = TextButtonWidget | IconButtonWidget | SeparatorWidget | PopoverButtonWidget | NumberInputWidget;
|
||||
export type WidgetRow = Array<Widgets>;
|
||||
export type WidgetLayout = Array<WidgetRow>;
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
}
|
||||
|
||||
.workspace-row {
|
||||
position: relative;
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<MenuBarInput v-if="platform !== ApplicationPlatform.Mac" />
|
||||
</div>
|
||||
<div class="header-third">
|
||||
<WindowTitle :title="'Untitled Document.gdd* - Graphite'" />
|
||||
<WindowTitle :title="`${document.title}${document.unsaved ? '*' : ''} - Graphite`" />
|
||||
</div>
|
||||
<div class="header-third">
|
||||
<WindowButtonsWindows :maximized="maximized" v-if="platform === ApplicationPlatform.Windows || platform === ApplicationPlatform.Linux" />
|
||||
|
|
@ -41,6 +41,7 @@ import MenuBarInput from "@/components/widgets/inputs/MenuBarInput.vue";
|
|||
import { ApplicationPlatform } from "@/components/window/MainWindow.vue";
|
||||
|
||||
export default defineComponent({
|
||||
inject: ["document"],
|
||||
props: {
|
||||
platform: { type: String, required: true },
|
||||
maximized: { type: Boolean, required: true },
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
class="tab"
|
||||
:class="{ active: tabIndex === tabActiveIndex }"
|
||||
v-for="(tabLabel, tabIndex) in tabLabels"
|
||||
:key="tabLabel"
|
||||
:key="tabIndex"
|
||||
@click.middle="handleTabClose(tabIndex)"
|
||||
@click="handleTabClick(tabIndex)"
|
||||
>
|
||||
|
|
@ -142,7 +142,10 @@
|
|||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { defineComponent, PropType } from "vue";
|
||||
|
||||
import { createDialog, dismissDialog } from "@/utilities/dialog";
|
||||
|
||||
import Document from "@/components/panels/Document.vue";
|
||||
import Properties from "@/components/panels/Properties.vue";
|
||||
import LayerTree from "@/components/panels/LayerTree.vue";
|
||||
|
|
@ -155,6 +158,7 @@ import { ResponseType, registerResponseHandler, Response, DisplayConfirmationToC
|
|||
const wasm = import("@/../wasm/pkg");
|
||||
|
||||
export default defineComponent({
|
||||
inject: ["dialog"],
|
||||
components: {
|
||||
Document,
|
||||
Properties,
|
||||
|
|
@ -175,14 +179,54 @@ export default defineComponent({
|
|||
select_document(tabIndex);
|
||||
},
|
||||
async closeDocumentWithConfirmation(tabIndex: number) {
|
||||
// eslint-disable-next-line no-alert
|
||||
const userConfirmation = window.confirm("Closing this document will permanently discard all work. Continue?");
|
||||
if (userConfirmation) (await wasm).close_document(tabIndex);
|
||||
this.selectDocument(tabIndex);
|
||||
const tabLabel = this.tabLabels[tabIndex];
|
||||
|
||||
// TODO: Rename to "Save changes before closing?" when we can actually save documents somewhere, not just export SVGs
|
||||
createDialog("File", "Close without exporting SVG?", tabLabel, [
|
||||
{
|
||||
kind: "TextButton",
|
||||
callback: async () => {
|
||||
(await wasm).export_document();
|
||||
dismissDialog();
|
||||
},
|
||||
props: { label: "Export", emphasized: true, minWidth: 96 },
|
||||
},
|
||||
{
|
||||
kind: "TextButton",
|
||||
callback: async () => {
|
||||
(await wasm).close_document(tabIndex);
|
||||
dismissDialog();
|
||||
},
|
||||
props: { label: "Discard", minWidth: 96 },
|
||||
},
|
||||
{
|
||||
kind: "TextButton",
|
||||
callback: async () => {
|
||||
dismissDialog();
|
||||
},
|
||||
props: { label: "Cancel", minWidth: 96 },
|
||||
},
|
||||
]);
|
||||
},
|
||||
async closeAllDocumentsWithConfirmation() {
|
||||
// eslint-disable-next-line no-alert
|
||||
const userConfirmation = window.confirm("Closing all documents will permanently discard all work in each of them. Continue?");
|
||||
if (userConfirmation) (await wasm).close_all_documents();
|
||||
createDialog("Copy", "Close all documents?", "Unsaved work will be lost!", [
|
||||
{
|
||||
kind: "TextButton",
|
||||
callback: async () => {
|
||||
(await wasm).close_all_documents();
|
||||
dismissDialog();
|
||||
},
|
||||
props: { label: "Discard All", minWidth: 96 },
|
||||
},
|
||||
{
|
||||
kind: "TextButton",
|
||||
callback: async () => {
|
||||
dismissDialog();
|
||||
},
|
||||
props: { label: "Cancel", minWidth: 96 },
|
||||
},
|
||||
]);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
|
@ -198,7 +242,7 @@ export default defineComponent({
|
|||
props: {
|
||||
tabMinWidths: { type: Boolean, default: false },
|
||||
tabCloseButtons: { type: Boolean, default: false },
|
||||
tabLabels: { type: Array, required: true },
|
||||
tabLabels: { type: Array as PropType<string[]>, required: true },
|
||||
tabActiveIndex: { type: Number, required: true },
|
||||
panelType: { type: String, required: true },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
</LayoutRow>
|
||||
<DialogModal v-if="dialog.visible" />
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
@ -46,35 +47,43 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
import { setDocumentTitle } from "@/utilities/document";
|
||||
|
||||
import Panel from "@/components/workspace/Panel.vue";
|
||||
import { ResponseType, registerResponseHandler, Response, SetActiveDocument, UpdateOpenDocumentsList } from "@/utilities/response-handler";
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||
import DialogModal from "@/components/widgets/floating-menus/DialogModal.vue";
|
||||
|
||||
const wasm = import("@/../wasm/pkg");
|
||||
|
||||
export default defineComponent({
|
||||
inject: ["dialog"],
|
||||
components: {
|
||||
LayoutRow,
|
||||
LayoutCol,
|
||||
Panel,
|
||||
DialogModal,
|
||||
},
|
||||
|
||||
mounted() {
|
||||
registerResponseHandler(ResponseType.UpdateOpenDocumentsList, (responseData: Response) => {
|
||||
const documentListData = responseData as UpdateOpenDocumentsList;
|
||||
if (documentListData) {
|
||||
this.documents = documentListData.open_documents;
|
||||
setDocumentTitle(this.documents[this.activeDocument]);
|
||||
}
|
||||
});
|
||||
registerResponseHandler(ResponseType.SetActiveDocument, (responseData: Response) => {
|
||||
const documentData = responseData as SetActiveDocument;
|
||||
if (documentData) this.activeDocument = documentData.document_index;
|
||||
if (documentData) {
|
||||
this.activeDocument = documentData.document_index;
|
||||
setDocumentTitle(this.documents[this.activeDocument]);
|
||||
}
|
||||
});
|
||||
|
||||
(async () => (await wasm).get_open_documents_list())();
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
activeDocument: 0,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { createApp } from "vue";
|
||||
import { fullscreenModeChanged } from "@/utilities/fullscreen";
|
||||
import { handleKeyUp, handleKeyDown } from "@/utilities/input";
|
||||
import { handleKeyUp, handleKeyDown, handleMouseDown } from "@/utilities/input";
|
||||
import App from "@/App.vue";
|
||||
|
||||
// Bind global browser events
|
||||
|
|
@ -8,6 +8,7 @@ document.addEventListener("contextmenu", (e) => e.preventDefault());
|
|||
document.addEventListener("fullscreenchange", () => fullscreenModeChanged());
|
||||
window.addEventListener("keyup", (e: KeyboardEvent) => handleKeyUp(e));
|
||||
window.addEventListener("keydown", (e: KeyboardEvent) => handleKeyDown(e));
|
||||
window.addEventListener("mousedown", (e: MouseEvent) => handleMouseDown(e));
|
||||
|
||||
// Initialize the Vue application
|
||||
createApp(App).mount("#app");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
import { createDialog, dismissDialog } from "@/utilities/dialog";
|
||||
import { TextButtonWidget } from "@/components/widgets/widgets";
|
||||
|
||||
export default function comingSoon(issueNumber?: number) {
|
||||
const bugMessage = `— but you can help add it!\nSee issue #${issueNumber} on GitHub.`;
|
||||
const details = `This feature is not implemented yet${issueNumber ? bugMessage : ""}`;
|
||||
|
||||
const okButton: TextButtonWidget = {
|
||||
kind: "TextButton",
|
||||
callback: async () => dismissDialog(),
|
||||
props: { label: "OK", emphasized: true, minWidth: 96 },
|
||||
};
|
||||
const issueButton: TextButtonWidget = {
|
||||
kind: "TextButton",
|
||||
callback: async () => window.open(`https://github.com/GraphiteEditor/Graphite/issues/${issueNumber}`, "_blank"),
|
||||
props: { label: `Issue #${issueNumber}`, minWidth: 96 },
|
||||
};
|
||||
const buttons = [okButton];
|
||||
if (issueNumber) buttons.push(issueButton);
|
||||
|
||||
createDialog("Warning", "Coming soon", details, buttons);
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { reactive, readonly } from "vue";
|
||||
|
||||
import { TextButtonWidget } from "@/components/widgets/widgets";
|
||||
|
||||
const state = reactive({
|
||||
visible: false,
|
||||
icon: "",
|
||||
heading: "",
|
||||
details: "",
|
||||
buttons: [] as TextButtonWidget[],
|
||||
});
|
||||
|
||||
export function createDialog(icon: string, heading: string, details: string, buttons: TextButtonWidget[]) {
|
||||
state.visible = true;
|
||||
state.icon = icon;
|
||||
state.heading = heading;
|
||||
state.details = details;
|
||||
state.buttons = buttons;
|
||||
}
|
||||
|
||||
export function dismissDialog() {
|
||||
state.visible = false;
|
||||
}
|
||||
|
||||
export function submitDialog() {
|
||||
const firstEmphasizedButton = state.buttons.find((button) => button.props.emphasized && button.callback);
|
||||
if (firstEmphasizedButton) {
|
||||
// If statement satisfies TypeScript
|
||||
if (firstEmphasizedButton.callback) firstEmphasizedButton.callback();
|
||||
}
|
||||
}
|
||||
|
||||
export function dialogIsVisible(): boolean {
|
||||
return state.visible;
|
||||
}
|
||||
|
||||
export default readonly(state);
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { reactive, readonly } from "vue";
|
||||
|
||||
const state = reactive({
|
||||
title: "",
|
||||
unsaved: false,
|
||||
});
|
||||
|
||||
export function setDocumentTitle(title: string) {
|
||||
state.title = title;
|
||||
}
|
||||
|
||||
export function setUnsavedState(isUnsaved: boolean) {
|
||||
state.unsaved = isUnsaved;
|
||||
}
|
||||
|
||||
export function documentTitle(): string {
|
||||
return state.title;
|
||||
}
|
||||
|
||||
export function documentIsUnsaved(): boolean {
|
||||
return state.unsaved;
|
||||
}
|
||||
|
||||
export default readonly(state);
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { toggleFullscreen } from "@/utilities/fullscreen";
|
||||
import { dialogIsVisible, dismissDialog, submitDialog } from "@/utilities/dialog";
|
||||
|
||||
const wasm = import("@/../wasm/pkg");
|
||||
|
||||
|
|
@ -7,6 +8,9 @@ export function shouldRedirectKeyboardEventToBackend(e: KeyboardEvent): boolean
|
|||
const target = e.target as HTMLElement;
|
||||
if (target.nodeName === "INPUT" || target.nodeName === "TEXTAREA" || target.isContentEditable) return false;
|
||||
|
||||
// Don't redirect when a modal is covering the workspace
|
||||
if (dialogIsVisible()) return false;
|
||||
|
||||
// Don't redirect a fullscreen request
|
||||
if (e.key.toLowerCase() === "f11" && e.type === "keydown" && !e.repeat) {
|
||||
e.preventDefault();
|
||||
|
|
@ -33,6 +37,15 @@ export async function handleKeyDown(e: KeyboardEvent) {
|
|||
const { on_key_down } = await wasm;
|
||||
const modifiers = makeModifiersBitfield(e.ctrlKey, e.shiftKey, e.altKey);
|
||||
on_key_down(e.key, modifiers);
|
||||
return;
|
||||
}
|
||||
|
||||
if (dialogIsVisible()) {
|
||||
if (e.key === "Escape") dismissDialog();
|
||||
if (e.key === "Enter") submitDialog();
|
||||
|
||||
// Prevent the Enter key from acting like a click on the last clicked button, which might reopen the dialog
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -45,6 +58,18 @@ export async function handleKeyUp(e: KeyboardEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function handleMouseDown(e: MouseEvent) {
|
||||
const target = e.target && (e.target as HTMLElement);
|
||||
const clickedInsideDialog = target && target.closest(".dialog-modal .floating-menu-content");
|
||||
|
||||
if (dialogIsVisible() && !clickedInsideDialog) {
|
||||
dismissDialog();
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
export function makeModifiersBitfield(control: boolean, shift: boolean, alt: boolean): number {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return Number(control) | (Number(shift) << 1) | (Number(alt) << 2);
|
||||
|
|
|
|||
|
|
@ -194,7 +194,6 @@ impl Default for Mapping {
|
|||
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyDelete},
|
||||
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyX},
|
||||
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyBackspace},
|
||||
entry! {action=DocumentMessage::ExportDocument, key_down=KeyS, modifiers=[KeyControl, KeyShift]},
|
||||
entry! {action=DocumentMessage::ExportDocument, key_down=KeyE, modifiers=[KeyControl]},
|
||||
entry! {action=DocumentMessage::MouseMove, message=InputMapperMessage::PointerMove},
|
||||
entry! {action=DocumentMessage::RotateCanvasBegin{snap:false}, key_down=Mmb, modifiers=[KeyControl]},
|
||||
|
|
|
|||
Loading…
Reference in New Issue