Menu bar definition in backend (#681)

* initial menu layout working

* removed api.rs functions

* no action shortcut for no-op

* code review

* nitpicks
This commit is contained in:
mfish33 2022-06-18 18:46:23 -06:00 committed by Keavon Chambers
parent 9bd27ec3f8
commit 5d6d2b22bc
35 changed files with 781 additions and 475 deletions

View file

@ -2,7 +2,7 @@
<template>
<div class="widget-layout">
<component :is="layoutRowType(layoutRow)" :widgetData="layoutRow" :layoutTarget="layout.layout_target" v-for="(layoutRow, index) in layout.layout" :key="index" />
<component :is="LayoutGroupType(layoutRow)" :widgetData="layoutRow" :layoutTarget="layout.layout_target" v-for="(layoutRow, index) in layout.layout" :key="index" />
</div>
</template>
@ -18,7 +18,7 @@
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { isWidgetColumn, isWidgetRow, isWidgetSection, LayoutRow, WidgetLayout } from "@/wasm-communication/messages";
import { isWidgetColumn, isWidgetRow, isWidgetSection, LayoutGroup, WidgetLayout } from "@/wasm-communication/messages";
import WidgetSection from "@/components/widgets/groups/WidgetSection.vue";
import WidgetRow from "@/components/widgets/WidgetRow.vue";
@ -28,7 +28,7 @@ export default defineComponent({
layout: { type: Object as PropType<WidgetLayout>, required: true },
},
methods: {
layoutRowType(layoutRow: LayoutRow): unknown {
LayoutGroupType(layoutRow: LayoutGroup): unknown {
if (isWidgetColumn(layoutRow)) return WidgetRow;
if (isWidgetRow(layoutRow)) return WidgetRow;
if (isWidgetSection(layoutRow)) return WidgetSection;

View file

@ -7,7 +7,7 @@
<TextLabel :bold="true">{{ widgetData.name }}</TextLabel>
</button>
<LayoutCol class="body" v-if="expanded">
<component :is="layoutRowType(layoutRow)" :widgetData="layoutRow" :layoutTarget="layoutTarget" v-for="(layoutRow, index) in widgetData.layout" :key="index"></component>
<component :is="layoutGroupType(layoutRow)" :widgetData="layoutRow" :layoutTarget="layoutTarget" v-for="(layoutRow, index) in widgetData.layout" :key="index"></component>
</LayoutCol>
</LayoutCol>
</template>
@ -72,7 +72,7 @@
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { isWidgetRow, isWidgetSection, LayoutRow as LayoutSystemRow, WidgetSection as WidgetSectionFromJsMessages } from "@/wasm-communication/messages";
import { isWidgetRow, isWidgetSection, LayoutGroup, WidgetSection as WidgetSectionFromJsMessages } from "@/wasm-communication/messages";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
@ -96,9 +96,9 @@ const WidgetSection = defineComponent({
updateLayout(widgetId: bigint, value: unknown) {
this.editor.instance.update_layout(this.layoutTarget, widgetId, value);
},
layoutRowType(layoutRow: LayoutSystemRow): unknown {
if (isWidgetRow(layoutRow)) return WidgetRow;
if (isWidgetSection(layoutRow)) return WidgetSection;
layoutGroupType(layoutGroup: LayoutGroup): unknown {
if (isWidgetRow(layoutGroup)) return WidgetRow;
if (isWidgetSection(layoutGroup)) return WidgetSection;
throw new Error("Layout row type does not exist");
},

View file

@ -72,151 +72,42 @@
<script lang="ts">
import { defineComponent } from "vue";
import { Editor } from "@/wasm-communication/editor";
import { MenuEntry, UpdateMenuBarLayout } from "@/wasm-communication/messages";
import MenuList, { MenuListEntry, MenuListEntries } from "@/components/floating-menus/MenuList.vue";
import MenuList, { MenuListEntry } from "@/components/floating-menus/MenuList.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
function makeEntries(editor: Editor): MenuListEntries {
return [
{
label: "File",
ref: undefined,
children: [
[
{ label: "New…", icon: "File", shortcut: ["KeyControl", "KeyN"], shortcutRequiresLock: true, action: (): void => editor.instance.request_new_document_dialog() },
{ label: "Open…", shortcut: ["KeyControl", "KeyO"], action: (): void => editor.instance.open_document() },
{
label: "Open Recent",
shortcut: ["KeyControl", "KeyShift", "KeyO"],
action: (): void => undefined,
children: [
[{ label: "Reopen Last Closed", shortcut: ["KeyControl", "KeyShift", "KeyT"], shortcutRequiresLock: true }, { label: "Clear Recently Opened" }],
[
{ label: "Some Recent File.gdd" },
{ label: "Another Recent File.gdd" },
{ label: "An Older File.gdd" },
{ label: "Some Other Older File.gdd" },
{ label: "Yet Another Older File.gdd" },
],
],
},
],
[
{ label: "Close", shortcut: ["KeyControl", "KeyW"], shortcutRequiresLock: true, action: async (): Promise<void> => editor.instance.close_active_document_with_confirmation() },
{ label: "Close All", shortcut: ["KeyControl", "KeyAlt", "KeyW"], action: async (): Promise<void> => editor.instance.close_all_documents_with_confirmation() },
],
[
{ label: "Save", shortcut: ["KeyControl", "KeyS"], action: async (): Promise<void> => editor.instance.save_document() },
{ label: "Save As…", shortcut: ["KeyControl", "KeyShift", "KeyS"], action: async (): Promise<void> => editor.instance.save_document() },
{ label: "Save All", shortcut: ["KeyControl", "KeyAlt", "KeyS"] },
{ label: "Auto-Save", icon: "CheckboxChecked" },
],
[
{ label: "Import…", shortcut: ["KeyControl", "KeyI"] },
{ label: "Export…", shortcut: ["KeyControl", "KeyE"], action: async (): Promise<void> => editor.instance.export_document() },
],
[{ label: "Quit", shortcut: ["KeyControl", "KeyQ"] }],
],
},
{
label: "Edit",
ref: undefined,
children: [
[
{ label: "Undo", shortcut: ["KeyControl", "KeyZ"], action: async (): Promise<void> => editor.instance.undo() },
{ label: "Redo", shortcut: ["KeyControl", "KeyShift", "KeyZ"], action: async (): Promise<void> => editor.instance.redo() },
],
[
{ label: "Cut", shortcut: ["KeyControl", "KeyX"], action: async (): Promise<void> => editor.instance.cut() },
{ label: "Copy", icon: "Copy", shortcut: ["KeyControl", "KeyC"], action: async (): Promise<void> => editor.instance.copy() },
// TODO: Fix this
// { label: "Paste", icon: "Paste", shortcut: ["KeyControl", "KeyV"], action: async (): Promise<void> => editor.instance.paste() },
],
],
},
{
label: "Layer",
ref: undefined,
children: [
[
{ label: "Select All", shortcut: ["KeyControl", "KeyA"], action: async (): Promise<void> => editor.instance.select_all_layers() },
{ label: "Deselect All", shortcut: ["KeyControl", "KeyAlt", "KeyA"], action: async (): Promise<void> => editor.instance.deselect_all_layers() },
{
label: "Order",
action: (): void => undefined,
children: [
[
{
label: "Raise To Front",
shortcut: ["KeyControl", "KeyShift", "KeyLeftBracket"],
action: async (): Promise<void> => editor.instance.reorder_selected_layers(editor.instance.i32_max()),
},
{ label: "Raise", shortcut: ["KeyControl", "KeyRightBracket"], action: async (): Promise<void> => editor.instance.reorder_selected_layers(1) },
{ label: "Lower", shortcut: ["KeyControl", "KeyLeftBracket"], action: async (): Promise<void> => editor.instance.reorder_selected_layers(-1) },
{
label: "Lower to Back",
shortcut: ["KeyControl", "KeyShift", "KeyRightBracket"],
action: async (): Promise<void> => editor.instance.reorder_selected_layers(editor.instance.i32_min()),
},
],
],
},
],
],
},
{
label: "Document",
ref: undefined,
children: [[{ label: "Menu entries coming soon" }]],
},
{
label: "View",
ref: undefined,
children: [
[
{
label: "Show/Hide Node Graph (In Development)",
action: async (): Promise<void> => editor.instance.toggle_node_graph_visibility(),
},
],
],
},
{
label: "Help",
ref: undefined,
children: [
[
{
label: "About Graphite",
action: async (): Promise<void> => editor.instance.request_about_graphite_dialog(),
},
],
[
{ label: "Report a Bug", action: (): unknown => window.open("https://github.com/GraphiteEditor/Graphite/issues/new", "_blank") },
{ label: "Visit on GitHub", action: (): unknown => window.open("https://github.com/GraphiteEditor/Graphite", "_blank") },
],
[
{
label: "Debug: Set Log Level",
action: (): void => undefined,
children: [
[
{ label: "Log Level Info", action: async (): Promise<void> => editor.instance.log_level_info(), shortcut: ["Key1"] },
{ label: "Log Level Debug", action: async (): Promise<void> => editor.instance.log_level_debug(), shortcut: ["Key2"] },
{ label: "Log Level Trace", action: async (): Promise<void> => editor.instance.log_level_trace(), shortcut: ["Key3"] },
],
],
},
{ label: "Debug: Panic (DANGER)", action: async (): Promise<void> => editor.instance.intentional_panic() },
],
],
},
];
}
const LOCK_REQUIRING_SHORTCUTS = [
["KeyControl", "KeyN"],
["KeyControl", "KeyShift", "KeyT"],
["KeyControl", "KeyW"],
];
type FrontendMenuColumn = {
label: string;
children: FrontendMenuEntry[][];
};
type FrontendMenuEntry = Omit<MenuEntry, "action" | "children"> & { shortcutRequiresLock: boolean | undefined; action: () => void; children: FrontendMenuEntry[][] | undefined };
export default defineComponent({
inject: ["editor"],
mounted() {
this.editor.subscriptions.subscribeJsMessage(UpdateMenuBarLayout, (updateMenuBarLayout) => {
const shortcutRequiresLock = (shortcut: string[]): boolean => LOCK_REQUIRING_SHORTCUTS.some((lockKeyCombo) => shortcut.every((shortcutKey, index) => shortcutKey === lockKeyCombo[index]));
const menuEntryToFrontendMenuEntry = (subLayout: MenuEntry[][]): FrontendMenuEntry[][] =>
subLayout.map((group) =>
group.map((entry) => ({
...entry,
children: entry.children ? menuEntryToFrontendMenuEntry(entry.children) : undefined,
action: (): void => this.editor.instance.update_layout(updateMenuBarLayout.layout_target, entry.action.widget_id, undefined),
shortcutRequiresLock: entry.shortcut ? shortcutRequiresLock(entry.shortcut) : undefined,
}))
);
this.entries = updateMenuBarLayout.layout.map((column) => ({ ...column, children: menuEntryToFrontendMenuEntry(column.children) }));
});
},
methods: {
onClick(menuEntry: MenuListEntry, target: EventTarget | null) {
// Focus the target so that keyboard inputs are sent to the dropdown
@ -236,7 +127,7 @@ export default defineComponent({
},
data() {
return {
entries: makeEntries(this.editor),
entries: [] as FrontendMenuColumn[],
open: false,
};
},

View file

@ -74,9 +74,9 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
// Don't redirect debugging tools
if (key === "f12" || key === "f8") return false;
if (e.ctrlKey && e.shiftKey && key === "c") return false;
if (e.ctrlKey && e.shiftKey && key === "i") return false;
if (e.ctrlKey && e.shiftKey && key === "j") return false;
if ((e.ctrlKey || e.metaKey) && e.shiftKey && key === "c") return false;
if ((e.ctrlKey || e.metaKey) && e.shiftKey && key === "i") return false;
if ((e.ctrlKey || e.metaKey) && e.shiftKey && key === "j") return false;
// Don't redirect tab or enter if not in canvas (to allow navigating elements)
if (!canvasFocused && !targetIsTextField(e.target) && ["tab", "enter", " ", "arrowdown", "arrowup", "arrowleft", "arrowright"].includes(key.toLowerCase())) return false;

View file

@ -370,7 +370,7 @@ export class TriggerVisitLink extends JsMessage {
}
export interface WidgetLayout {
layout: LayoutRow[];
layout: LayoutGroup[];
layout_target: unknown;
}
@ -381,21 +381,20 @@ export function defaultWidgetLayout(): WidgetLayout {
};
}
// TODO: Rename LayoutRow to something more generic
export type LayoutRow = WidgetRow | WidgetColumn | WidgetSection;
export type LayoutGroup = WidgetRow | WidgetColumn | WidgetSection;
export type WidgetColumn = { columnWidgets: Widget[] };
export function isWidgetColumn(layoutColumn: LayoutRow): layoutColumn is WidgetColumn {
export function isWidgetColumn(layoutColumn: LayoutGroup): layoutColumn is WidgetColumn {
return Boolean((layoutColumn as WidgetColumn).columnWidgets);
}
export type WidgetRow = { rowWidgets: Widget[] };
export function isWidgetRow(layoutRow: LayoutRow): layoutRow is WidgetRow {
export function isWidgetRow(layoutRow: LayoutGroup): layoutRow is WidgetRow {
return Boolean((layoutRow as WidgetRow).rowWidgets);
}
export type WidgetSection = { name: string; layout: LayoutRow[] };
export function isWidgetSection(layoutRow: LayoutRow): layoutRow is WidgetSection {
export type WidgetSection = { name: string; layout: LayoutGroup[] };
export function isWidgetSection(layoutRow: LayoutGroup): layoutRow is WidgetSection {
return Boolean((layoutRow as WidgetSection).layout);
}
@ -427,86 +426,72 @@ export class UpdateDialogDetails extends JsMessage implements WidgetLayout {
layout_target!: unknown;
@Transform(({ value }) => createWidgetLayout(value))
layout!: LayoutRow[];
layout!: LayoutGroup[];
}
export class UpdateDocumentModeLayout extends JsMessage implements WidgetLayout {
layout_target!: unknown;
@Transform(({ value }) => createWidgetLayout(value))
layout!: LayoutRow[];
layout!: LayoutGroup[];
}
export class UpdateToolOptionsLayout extends JsMessage implements WidgetLayout {
layout_target!: unknown;
@Transform(({ value }) => createWidgetLayout(value))
layout!: LayoutRow[];
layout!: LayoutGroup[];
}
export class UpdateDocumentBarLayout extends JsMessage implements WidgetLayout {
layout_target!: unknown;
@Transform(({ value }) => createWidgetLayout(value))
layout!: LayoutRow[];
layout!: LayoutGroup[];
}
export class UpdateToolShelfLayout extends JsMessage implements WidgetLayout {
layout_target!: unknown;
@Transform(({ value }) => createWidgetLayout(value))
layout!: LayoutRow[];
layout!: LayoutGroup[];
}
export class UpdatePropertyPanelOptionsLayout extends JsMessage implements WidgetLayout {
layout_target!: unknown;
@Transform(({ value }) => createWidgetLayout(value))
layout!: LayoutRow[];
layout!: LayoutGroup[];
}
export class UpdatePropertyPanelSectionsLayout extends JsMessage implements WidgetLayout {
layout_target!: unknown;
@Transform(({ value }) => createWidgetLayout(value))
layout!: LayoutRow[];
layout!: LayoutGroup[];
}
export class UpdateLayerTreeOptionsLayout extends JsMessage implements WidgetLayout {
layout_target!: unknown;
@Transform(({ value }) => createWidgetLayout(value))
layout!: LayoutRow[];
layout!: LayoutGroup[];
}
// Unpacking rust types to more usable type in the frontend
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createWidgetLayout(widgetLayout: any[]): LayoutRow[] {
return widgetLayout.map((layoutType): LayoutRow => {
function createWidgetLayout(widgetLayout: any[]): LayoutGroup[] {
return widgetLayout.map((layoutType): LayoutGroup => {
if (layoutType.Column) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const columnWidgets = layoutType.Column.columnWidgets.map((widgetHolder: any) => {
const { widget_id } = widgetHolder;
const kind = Object.keys(widgetHolder.widget)[0];
const props = widgetHolder.widget[kind];
return { widget_id, kind, props };
});
const columnWidgets = hoistWidgetHolders(layoutType.Column.columnWidgets);
const result: WidgetColumn = { columnWidgets };
return result;
}
if (layoutType.Row) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rowWidgets = layoutType.Row.rowWidgets.map((widgetHolder: any) => {
const { widget_id } = widgetHolder;
const kind = Object.keys(widgetHolder.widget)[0];
const props = widgetHolder.widget[kind];
return { widget_id, kind, props };
});
const rowWidgets = hoistWidgetHolders(layoutType.Row.rowWidgets);
const result: WidgetRow = { rowWidgets };
return result;
}
@ -522,6 +507,52 @@ function createWidgetLayout(widgetLayout: any[]): LayoutRow[] {
throw new Error("Layout row type does not exist");
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function hoistWidgetHolders(widgetHolders: any[]): Widget[] {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return widgetHolders.map((widgetHolder: any) => {
const { widget_id } = widgetHolder;
const kind = Object.keys(widgetHolder.widget)[0];
const props = widgetHolder.widget[kind];
return { widget_id, kind, props } as Widget;
});
}
export class UpdateMenuBarLayout extends JsMessage {
layout_target!: unknown;
@Transform(({ value }) => createMenuLayout(value))
layout!: MenuColumn[];
}
export type MenuEntry = {
shortcut: string[] | undefined;
action: Widget;
label: string;
icon: string | undefined;
children: undefined | MenuEntry[][];
};
export type MenuColumn = {
label: string;
children: MenuEntry[][];
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createMenuLayout(menuLayout: any[]): MenuColumn[] {
return menuLayout.map((column) => ({ ...column, children: createMenuLayoutRecursive(column.children) }));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createMenuLayoutRecursive(subLayout: any[][]): MenuEntry[][] {
return subLayout.map((groups) =>
groups.map((entry) => ({
...entry,
action: hoistWidgetHolders([entry.action])[0],
children: entry.children ? createMenuLayoutRecursive(entry.children) : undefined,
}))
);
}
export class TriggerTextCommit extends JsMessage {}
@ -579,5 +610,6 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateDocumentModeLayout,
UpdateToolOptionsLayout,
UpdateWorkingColors,
UpdateMenuBarLayout,
} as const;
export type JsMessageType = keyof typeof messageMakers;