mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-21 14:08:12 +08:00
Refactor the old menu bar plumbing to use standard TextButtons (#3444)
* Refactor the old menu bar plumbing to use standard TextButtons * WIP: Fix Mac native menu bar * WIP: fix desktop menu bar mac * Refactor menu bar definitions to use the builder pattern * WIP: fixup desktop * cleanup * fix linux * Remove dead code that was failing to lint --------- Co-authored-by: Timon Schelling <me@timon.zip>
This commit is contained in:
co-authored by
Timon Schelling
parent
3fd0460d03
commit
600fb5c28f
@@ -19,8 +19,16 @@
|
||||
let scroller: LayoutCol | undefined;
|
||||
let searchTextInput: TextInput | undefined;
|
||||
|
||||
const dispatch = createEventDispatcher<{ open: boolean; activeEntry: MenuListEntry; hoverInEntry: MenuListEntry; hoverOutEntry: undefined; naturalWidth: number }>();
|
||||
const dispatch = createEventDispatcher<{
|
||||
open: boolean;
|
||||
activeEntry: MenuListEntry;
|
||||
selectedEntryValuePath: string[];
|
||||
hoverInEntry: MenuListEntry;
|
||||
hoverOutEntry: undefined;
|
||||
naturalWidth: number;
|
||||
}>();
|
||||
|
||||
export let parentsValuePath: string[] = [];
|
||||
export let entries: MenuListEntry[][];
|
||||
export let activeEntry: MenuListEntry | undefined = undefined;
|
||||
export let open: boolean;
|
||||
@@ -30,9 +38,6 @@
|
||||
export let interactive = false;
|
||||
export let scrollableY = false;
|
||||
export let virtualScrollingEntryHeight = 0;
|
||||
export let tooltipLabel: string | undefined = undefined;
|
||||
export let tooltipDescription: string | undefined = undefined;
|
||||
export let tooltipShortcut: string | undefined = undefined;
|
||||
|
||||
// Keep the child references outside of the entries array so as to avoid infinite recursion.
|
||||
let childReferences: MenuList[][] = [];
|
||||
@@ -149,11 +154,9 @@
|
||||
}
|
||||
|
||||
function onEntryClick(menuListEntry: MenuListEntry) {
|
||||
// Call the action if available
|
||||
if (menuListEntry.action) menuListEntry.action();
|
||||
|
||||
// Notify the parent about the clicked entry as the new active entry
|
||||
dispatch("activeEntry", menuListEntry);
|
||||
dispatch("selectedEntryValuePath", [...parentsValuePath, menuListEntry.value]);
|
||||
|
||||
// Close the containing menu
|
||||
let childReference = getChildReference(menuListEntry);
|
||||
@@ -425,9 +428,9 @@
|
||||
class="row"
|
||||
classes={{ open: isEntryOpen(entry), active: entry.label === highlighted?.label, disabled: Boolean(entry.disabled) }}
|
||||
styles={{ height: virtualScrollingEntryHeight || "20px" }}
|
||||
{tooltipLabel}
|
||||
{tooltipDescription}
|
||||
{tooltipShortcut}
|
||||
tooltipLabel={entry.tooltipLabel}
|
||||
tooltipDescription={entry.tooltipDescription}
|
||||
tooltipShortcut={entry.tooltipShortcut}
|
||||
on:click={() => !entry.disabled && onEntryClick(entry)}
|
||||
on:pointerenter={() => !entry.disabled && onEntryPointerEnter(entry)}
|
||||
on:pointerleave={() => !entry.disabled && onEntryPointerLeave(entry)}
|
||||
@@ -444,8 +447,8 @@
|
||||
|
||||
<TextLabel class="entry-label" styles={{ "font-family": `${!entry.font ? "inherit" : entry.value}` }}>{entry.label}</TextLabel>
|
||||
|
||||
{#if entry.shortcut?.keys.length}
|
||||
<UserInputLabel keysWithLabelsGroups={[entry.shortcut.keys]} requiresLock={entry.shortcutRequiresLock} textOnly={true} />
|
||||
{#if entry.shortcutKeys?.keys.length}
|
||||
<UserInputLabel keysWithLabelsGroups={[entry.shortcutKeys.keys]} requiresLock={entry.shortcutRequiresLock} textOnly={true} />
|
||||
{/if}
|
||||
|
||||
{#if entry.children?.length}
|
||||
@@ -462,6 +465,8 @@
|
||||
// See explanation at <https://github.com/sveltejs/language-tools/issues/452#issuecomment-723148184>.
|
||||
dispatch("naturalWidth", detail);
|
||||
}}
|
||||
on:selectedEntryValuePath={({ detail }) => dispatch("selectedEntryValuePath", detail)}
|
||||
parentsValuePath={[...parentsValuePath, entry.value]}
|
||||
open={getChildReference(entry)?.open || false}
|
||||
direction="TopRight"
|
||||
entries={entry.children}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import type { FrontendNodeType } from "@graphite/messages";
|
||||
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
|
||||
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
@@ -109,8 +110,8 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="node-catalog">
|
||||
<TextInput placeholder="Search Nodes..." value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} />
|
||||
<LayoutCol class="node-catalog">
|
||||
<TextInput placeholder="Search Nodes…" value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} />
|
||||
<div class="list-results" on:wheel|passive|stopPropagation>
|
||||
{#each nodeCategories as nodeCategory}
|
||||
<details open={nodeCategory[1].open}>
|
||||
@@ -131,15 +132,12 @@
|
||||
<TextLabel>No search results</TextLabel>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</LayoutCol>
|
||||
|
||||
<style lang="scss" global>
|
||||
.node-catalog {
|
||||
max-height: 30vh;
|
||||
min-width: 250px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
.text-input {
|
||||
flex: 0 0 auto;
|
||||
@@ -149,13 +147,10 @@
|
||||
.list-results {
|
||||
overflow-y: auto;
|
||||
flex: 1 1 auto;
|
||||
// Together with the `margin-right: 4px;` on `details` below, this keeps a gap between the listings and the scrollbar
|
||||
margin-right: -4px;
|
||||
|
||||
details {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
// Together with the `margin-right: -4px;` on `.list-results` above, this keeps a gap between the listings and the scrollbar
|
||||
margin-right: 4px;
|
||||
|
||||
&[open] summary .text-label::before {
|
||||
@@ -164,8 +159,6 @@
|
||||
|
||||
summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
|
||||
.text-label {
|
||||
padding-left: 16px;
|
||||
@@ -189,6 +182,11 @@
|
||||
.text-button {
|
||||
width: 100%;
|
||||
margin: 4px 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&:last-child .text-button {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,13 +495,22 @@
|
||||
--floating-menu-content-offset: 0;
|
||||
|
||||
.tail {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
// Put the tail above the floating menu's shadow
|
||||
z-index: 10;
|
||||
// Draw over the application without being clipped by the containing panel's `overflow: hidden`
|
||||
position: fixed;
|
||||
|
||||
&,
|
||||
&::before {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
.floating-menu-container {
|
||||
@@ -510,6 +519,7 @@
|
||||
.floating-menu-content {
|
||||
background: var(--color-2-mildblack);
|
||||
box-shadow: rgba(var(--color-0-black-rgb), 0.5) 0 2px 4px;
|
||||
border: 1px solid var(--color-4-dimgray);
|
||||
border-radius: 4px;
|
||||
color: var(--color-e-nearwhite);
|
||||
font-size: inherit;
|
||||
@@ -517,6 +527,8 @@
|
||||
z-index: 0;
|
||||
// Draw over the application without being clipped by the containing panel's `overflow: hidden`
|
||||
position: fixed;
|
||||
// Counteract the rightward shift caused by the border
|
||||
margin-left: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -603,33 +615,69 @@
|
||||
&.top .tail,
|
||||
&.topleft .tail,
|
||||
&.topright .tail {
|
||||
border-width: 8px 6px 0 6px;
|
||||
border-color: var(--color-2-mildblack) transparent transparent transparent;
|
||||
margin-left: -6px;
|
||||
margin-bottom: 2px;
|
||||
border-color: var(--color-4-dimgray) transparent transparent transparent;
|
||||
|
||||
&::before {
|
||||
border-color: var(--color-2-mildblack) transparent transparent transparent;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&,
|
||||
&::before {
|
||||
border-width: 8px 6px 0 6px;
|
||||
margin-left: -6px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&.bottom .tail,
|
||||
&.bottomleft .tail,
|
||||
&.bottomright .tail {
|
||||
border-width: 0 6px 8px 6px;
|
||||
border-color: transparent transparent var(--color-2-mildblack) transparent;
|
||||
margin-left: -6px;
|
||||
margin-top: 2px;
|
||||
border-color: transparent transparent var(--color-4-dimgray) transparent;
|
||||
|
||||
&::before {
|
||||
border-color: transparent transparent var(--color-2-mildblack) transparent;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&,
|
||||
&::before {
|
||||
border-width: 0 6px 8px 6px;
|
||||
margin-left: -6px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&.left .tail {
|
||||
border-width: 6px 0 6px 8px;
|
||||
border-color: transparent transparent transparent var(--color-2-mildblack);
|
||||
margin-top: -6px;
|
||||
margin-right: 2px;
|
||||
border-color: transparent transparent transparent var(--color-4-dimgray);
|
||||
|
||||
&::before {
|
||||
border-color: transparent transparent transparent var(--color-2-mildblack);
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&,
|
||||
&::before {
|
||||
border-width: 6px 0 6px 8px;
|
||||
margin-top: -6px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&.right .tail {
|
||||
border-width: 6px 8px 6px 0;
|
||||
border-color: transparent var(--color-2-mildblack) transparent transparent;
|
||||
margin-top: -6px;
|
||||
margin-left: 2px;
|
||||
border-color: transparent var(--color-4-dimgray) transparent transparent;
|
||||
|
||||
&::before {
|
||||
border-color: transparent var(--color-2-mildblack) transparent transparent;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&,
|
||||
&::before {
|
||||
border-width: 6px 8px 6px 0;
|
||||
margin-top: -6px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&.top .floating-menu-container {
|
||||
|
||||
@@ -776,7 +776,7 @@
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.right-scrollbar .scrollbar-input {
|
||||
&:has(.top-ruler) .right-scrollbar .scrollbar-input {
|
||||
margin-top: -16px;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,13 +9,11 @@
|
||||
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
|
||||
|
||||
import NodeCatalog from "@graphite/components/floating-menus/NodeCatalog.svelte";
|
||||
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
|
||||
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
|
||||
import RadioInput from "@graphite/components/widgets/inputs/RadioInput.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
import Separator from "@graphite/components/widgets/labels/Separator.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
||||
const GRID_COLLAPSE_SPACING = 10;
|
||||
@@ -202,46 +200,44 @@
|
||||
>
|
||||
<!-- Right click menu for adding nodes -->
|
||||
{#if $nodeGraph.contextMenuInformation}
|
||||
<LayoutCol
|
||||
<FloatingMenu
|
||||
class="context-menu"
|
||||
data-context-menu
|
||||
styles={{
|
||||
left: `${$nodeGraph.contextMenuInformation.contextMenuCoordinates.x * $nodeGraph.transform.scale + $nodeGraph.transform.x}px`,
|
||||
top: `${$nodeGraph.contextMenuInformation.contextMenuCoordinates.y * $nodeGraph.transform.scale + $nodeGraph.transform.y}px`,
|
||||
}}
|
||||
open={true}
|
||||
type="Popover"
|
||||
direction="BottomLeft"
|
||||
>
|
||||
{#if $nodeGraph.contextMenuInformation.contextMenuData.type === "CreateNode"}
|
||||
<NodeCatalog initialSearchTerm={$nodeGraph.contextMenuInformation.contextMenuData.data.compatibleType || ""} on:selectNodeType={(e) => createNode(e.detail)} />
|
||||
{:else if $nodeGraph.contextMenuInformation.contextMenuData.type === "ModifyNode"}
|
||||
<LayoutRow class="toggle-layer-or-node">
|
||||
<TextLabel>Display as</TextLabel>
|
||||
<RadioInput
|
||||
selectedIndex={$nodeGraph.contextMenuInformation.contextMenuData.data.currentlyIsNode ? 0 : 1}
|
||||
entries={[
|
||||
{
|
||||
value: "node",
|
||||
label: "Node",
|
||||
action: () =>
|
||||
$nodeGraph.contextMenuInformation?.contextMenuData.type === "ModifyNode" &&
|
||||
editor.handle.setToNodeOrLayer($nodeGraph.contextMenuInformation.contextMenuData.data.nodeId, false),
|
||||
},
|
||||
{
|
||||
value: "layer",
|
||||
label: "Layer",
|
||||
action: () =>
|
||||
$nodeGraph.contextMenuInformation?.contextMenuData.type === "ModifyNode" &&
|
||||
editor.handle.setToNodeOrLayer($nodeGraph.contextMenuInformation.contextMenuData.data.nodeId, true),
|
||||
},
|
||||
]}
|
||||
disabled={!$nodeGraph.contextMenuInformation.contextMenuData.data.canBeLayer}
|
||||
<LayoutCol class="modify-node-menu">
|
||||
<TextButton
|
||||
label="Merge Selected Nodes"
|
||||
action={() => {
|
||||
editor.handle.mergeSelectedNodes();
|
||||
nodeGraph.closeContextMenu();
|
||||
}}
|
||||
flush={true}
|
||||
/>
|
||||
</LayoutRow>
|
||||
<Separator type="Section" direction="Vertical" />
|
||||
<LayoutRow class="merge-selected-nodes">
|
||||
<TextButton label="Merge Selected Nodes" action={() => editor.handle.mergeSelectedNodes()} />
|
||||
</LayoutRow>
|
||||
{@const currentlyIsNode = $nodeGraph.contextMenuInformation.contextMenuData.data.currentlyIsNode}
|
||||
<TextButton
|
||||
label={currentlyIsNode ? "Display as Layer" : "Display as Node"}
|
||||
action={() => {
|
||||
if ($nodeGraph.contextMenuInformation?.contextMenuData.type === "ModifyNode") {
|
||||
editor.handle.setToNodeOrLayer($nodeGraph.contextMenuInformation.contextMenuData.data.nodeId, currentlyIsNode);
|
||||
}
|
||||
nodeGraph.closeContextMenu();
|
||||
}}
|
||||
disabled={!$nodeGraph.contextMenuInformation.contextMenuData.data.canBeLayer}
|
||||
flush={true}
|
||||
/>
|
||||
</LayoutCol>
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
</FloatingMenu>
|
||||
{/if}
|
||||
|
||||
{#if $nodeGraph.error}
|
||||
@@ -822,20 +818,17 @@
|
||||
|
||||
.context-menu {
|
||||
width: max-content;
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
padding: 5px;
|
||||
z-index: 3;
|
||||
background-color: var(--color-3-darkgray);
|
||||
border-radius: 4px;
|
||||
|
||||
.toggle-layer-or-node .text-label {
|
||||
line-height: 24px;
|
||||
margin-right: 8px;
|
||||
.modify-node-menu {
|
||||
margin: -4px;
|
||||
|
||||
.text-button {
|
||||
justify-content: left;
|
||||
}
|
||||
}
|
||||
|
||||
.merge-selected-nodes {
|
||||
justify-content: center;
|
||||
.tail {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -968,11 +961,16 @@
|
||||
.imports-and-exports {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
// Keeps the connectors above the wires
|
||||
z-index: 1;
|
||||
|
||||
// Zero specificity with `:where()` to allow other rules to override `pointer-events`
|
||||
:where(.graph-view.open & > *) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.connector {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
@@ -1071,7 +1069,7 @@
|
||||
height: 100%;
|
||||
|
||||
// Zero specificity with `:where()` to allow other rules to override `pointer-events`
|
||||
:where(& > *) {
|
||||
:where(.graph-view.open & > *) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@
|
||||
{/if}
|
||||
{@const textButton = narrowWidgetProps(component.props, "TextButton")}
|
||||
{#if textButton}
|
||||
<TextButton {...exclude(textButton)} action={() => widgetValueCommitAndUpdate(index, undefined)} />
|
||||
<TextButton {...exclude(textButton)} action={() => widgetValueCommitAndUpdate(index, [])} on:selectedEntryValuePath={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
{/if}
|
||||
{@const breadcrumbTrailButtons = narrowWidgetProps(component.props, "BreadcrumbTrailButtons")}
|
||||
{#if breadcrumbTrailButtons}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { IconName } from "@graphite/icons";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { IconName } from "@graphite/icons";
|
||||
import type { MenuListEntry } from "@graphite/messages";
|
||||
|
||||
import MenuList from "@graphite/components/floating-menus/MenuList.svelte";
|
||||
@@ -8,6 +9,8 @@
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher<{ selectedEntryValuePath: string[] }>();
|
||||
|
||||
let self: MenuList;
|
||||
|
||||
// Note: IconButton should be used if only an icon, but no label, is desired.
|
||||
@@ -36,7 +39,7 @@
|
||||
// If there's no menu to open, trigger the action
|
||||
if ((menuListChildren?.length ?? 0) === 0) {
|
||||
// Call the action
|
||||
if (action && !disabled) action();
|
||||
if (!disabled) action?.();
|
||||
|
||||
// Exit early so we don't continue on and try to open the menu
|
||||
return;
|
||||
@@ -84,6 +87,7 @@
|
||||
{#if menuListChildrenExists}
|
||||
<MenuList
|
||||
on:open={({ detail }) => self && (self.open = detail)}
|
||||
on:selectedEntryValuePath={({ detail }) => dispatch("selectedEntryValuePath", detail)}
|
||||
open={self?.open || false}
|
||||
entries={menuListChildren || []}
|
||||
direction="Bottom"
|
||||
@@ -165,6 +169,11 @@
|
||||
&.open {
|
||||
--button-background-color: var(--color-5-dullgray);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
--button-text-color: var(--color-8-uppergray);
|
||||
--button-background-color: none;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-label {
|
||||
|
||||
@@ -84,8 +84,8 @@
|
||||
async function getEntries(): Promise<MenuListEntry[]> {
|
||||
const x = isStyle ? fonts.getFontStyles(fontFamily) : fonts.fontNames();
|
||||
return (await x).map((entry: { name: string; url: URL | undefined }) => ({
|
||||
label: entry.name,
|
||||
value: entry.name,
|
||||
label: entry.name,
|
||||
font: entry.url,
|
||||
action: () => selectFont(entry.name),
|
||||
}));
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
removeEventListener("keydown", trackCtrl);
|
||||
removeEventListener("keyup", trackCtrl);
|
||||
removeEventListener("mousemove", trackCtrl);
|
||||
clearTimeout(repeatTimeout);
|
||||
});
|
||||
|
||||
// ===============================
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
function handleEntryClick(radioEntryData: RadioEntryData) {
|
||||
const index = entries.indexOf(radioEntryData);
|
||||
dispatch("selectedIndex", index);
|
||||
|
||||
radioEntryData.action?.();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -203,6 +203,7 @@
|
||||
window.removeEventListener("pointermove", onPointerMove);
|
||||
window.removeEventListener("mousedown", onMouseDown);
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
clearTimeout(repeatTimeout);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@
|
||||
|
||||
.input-mouse {
|
||||
.bright {
|
||||
fill: var(--color-e-nearwhite);
|
||||
fill: var(--color-b-lightgray);
|
||||
}
|
||||
|
||||
.dim {
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
import { getContext, onMount } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { type KeyRaw, type LayoutKeysGroup, type MenuBarEntry, type MenuListEntry, UpdateMenuBarLayout } from "@graphite/messages";
|
||||
import { defaultWidgetLayout, patchWidgetLayout, UpdateMenuBarLayout } from "@graphite/messages";
|
||||
import type { AppWindowState } from "@graphite/state-providers/app-window";
|
||||
import { operatingSystem } from "@graphite/utility-functions/platform";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
|
||||
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
||||
import WindowButtonsLinux from "@graphite/components/window/title-bar/WindowButtonsLinux.svelte";
|
||||
import WindowButtonsWeb from "@graphite/components/window/title-bar/WindowButtonsWeb.svelte";
|
||||
import WindowButtonsWindows from "@graphite/components/window/title-bar/WindowButtonsWindows.svelte";
|
||||
@@ -15,44 +14,12 @@
|
||||
const appWindow = getContext<AppWindowState>("appWindow");
|
||||
const editor = getContext<Editor>("editor");
|
||||
|
||||
// TODO: Apparently, Safari does not support the Keyboard.lock() API but does relax its authority over certain keyboard shortcuts in fullscreen mode, which we should take advantage of
|
||||
const ACCEL_KEY = operatingSystem() === "Mac" ? "Command" : "Control";
|
||||
const LOCK_REQUIRING_SHORTCUTS: KeyRaw[][] = [
|
||||
[ACCEL_KEY, "KeyW"],
|
||||
[ACCEL_KEY, "KeyN"],
|
||||
[ACCEL_KEY, "Shift", "KeyN"],
|
||||
[ACCEL_KEY, "KeyT"],
|
||||
[ACCEL_KEY, "Shift", "KeyT"],
|
||||
];
|
||||
|
||||
let entries: MenuListEntry[] = [];
|
||||
let menuBarLayout = defaultWidgetLayout();
|
||||
|
||||
onMount(() => {
|
||||
const arraysEqual = (a: KeyRaw[], b: KeyRaw[]): boolean => a.length === b.length && a.every((aValue, i) => aValue === b[i]);
|
||||
const shortcutRequiresLock = (shortcut: LayoutKeysGroup): boolean => {
|
||||
const shortcutKeys = shortcut.map((keyWithLabel) => keyWithLabel.key);
|
||||
|
||||
// If this shortcut matches any of the browser-reserved shortcuts
|
||||
return LOCK_REQUIRING_SHORTCUTS.some((lockKeyCombo) => arraysEqual(shortcutKeys, lockKeyCombo));
|
||||
};
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateMenuBarLayout, (updateMenuBarLayout) => {
|
||||
const menuBarEntryToMenuListEntry = (entry: MenuBarEntry): MenuListEntry => ({
|
||||
// From `MenuEntryCommon`
|
||||
...entry,
|
||||
|
||||
// Shared names with fields that need to be converted from the type used in `MenuBarEntry` to that of `MenuListEntry`
|
||||
action: () => editor.handle.widgetValueCommitAndUpdate(updateMenuBarLayout.layoutTarget, entry.action.widgetId, undefined),
|
||||
children: entry.children ? entry.children.map((entries) => entries.map((entry) => menuBarEntryToMenuListEntry(entry))) : undefined,
|
||||
|
||||
// New fields in `MenuListEntry`
|
||||
shortcutRequiresLock: entry.shortcut ? shortcutRequiresLock(entry.shortcut.keys) : undefined,
|
||||
value: "",
|
||||
disabled: entry.disabled ?? undefined,
|
||||
font: undefined,
|
||||
});
|
||||
|
||||
entries = updateMenuBarLayout.layout.map(menuBarEntryToMenuListEntry);
|
||||
patchWidgetLayout(menuBarLayout, updateMenuBarLayout);
|
||||
menuBarLayout = menuBarLayout;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -61,9 +28,7 @@
|
||||
<!-- Menu bar -->
|
||||
<LayoutRow>
|
||||
{#if $appWindow.platform !== "Mac"}
|
||||
{#each entries as entry}
|
||||
<TextButton label={entry.label} icon={entry.icon} menuListChildren={entry.children} action={entry.action} flush={true} />
|
||||
{/each}
|
||||
<WidgetLayout layout={menuBarLayout} />
|
||||
{/if}
|
||||
</LayoutRow>
|
||||
<!-- Spacer -->
|
||||
@@ -88,6 +53,14 @@
|
||||
> .layout-row {
|
||||
flex: 0 0 auto;
|
||||
|
||||
> .widget-span {
|
||||
--row-height: 28px;
|
||||
|
||||
> * {
|
||||
--widget-height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
&.spacer {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user