Add a fullscreen button and the keyboard lock API (#266)

Closes #249
This commit is contained in:
Keavon Chambers 2021-07-14 18:56:22 -07:00
parent 3a520071cf
commit 0656f8abc5
14 changed files with 151 additions and 17 deletions

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<polygon points="11,8 11,11 8,11 8,12 12,12 12,8" />
<polygon points="1,1 4,1 4,0 0,0 0,4 1,4" />
<polygon points="4,11 1,11 1,8 0,8 0,12 4,12" />
<polygon points="11,1 11,4 12,4 12,0 8,0 8,1" />
</svg>

After

Width:  |  Height:  |  Size: 268 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<polygon points="9,12 9,9 12,9 12,8 8,8 8,12" />
<polygon points="3,3 0,3 0,4 4,4 4,0 3,0" />
<polygon points="0,9 3,9 3,12 4,12 4,8 0,8" />
<polygon points="9,3 9,0 8,0 8,4 12,4 12,3" />
</svg>

After

Width:  |  Height:  |  Size: 260 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<path d="M6,0C2.69,0,0,2.69,0,6s2.69,6,6,6s6-2.69,6-6S9.31,0,6,0z M6.35,11C5.71,11,5.2,10.49,5.2,9.85c0-0.63,0.51-1.15,1.15-1.15c0.63,0,1.15,0.51,1.15,1.15C7.49,10.49,6.98,11,6.35,11z M8.59,5.04C8.2,5.76,7.21,5.98,7.21,6.9v0.99h-1.8V6.9c0-1.89,1.88-1.62,1.88-3.12c0-1.41-2.16-1.88-2.77,0.15L3,3.46C4.25-0.96,10.66,1.2,8.59,5.04z" />
</svg>

After

Width:  |  Height:  |  Size: 402 B

View File

@ -196,10 +196,12 @@ img {
<script lang="ts">
import { defineComponent } from "vue";
import MainWindow from "./components/window/MainWindow.vue";
import LayoutRow from "./components/layout/LayoutRow.vue";
import fullscreen from "@/utilities/fullscreen";
import MainWindow from "@/components/window/MainWindow.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
export default defineComponent({
provide: { fullscreen },
data() {
return {
showUnsupportedModal: !("BigInt64Array" in window),

View File

@ -15,7 +15,8 @@
<Icon :icon="entry.icon" v-if="entry.icon && drawIcon" />
<div class="no-icon" v-else-if="drawIcon" />
<span class="entry-label">{{ entry.label }}</span>
<UserInputLabel v-if="entry.shortcut && entry.shortcut.length" :inputKeys="[entry.shortcut]" />
<Icon :icon="'Info'" v-if="entry.shortcutRequiresLock && !fullscreen.keyboardLocked" :title="keyboardLockInfoMessage" />
<UserInputLabel v-else-if="entry.shortcut && entry.shortcut.length" :inputKeys="[entry.shortcut]" />
<div class="submenu-arrow" v-if="entry.children && entry.children.length"></div>
<div class="no-submenu-arrow" v-else></div>
<MenuList
@ -121,6 +122,7 @@
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { keyboardLockApiSupported } from "@/utilities/fullscreen";
import FloatingMenu, { MenuDirection, MenuType } from "./FloatingMenu.vue";
import Separator, { SeparatorDirection, SeparatorType } from "../Separator.vue";
import Icon from "../labels/Icon.vue";
@ -134,13 +136,18 @@ interface MenuListEntryData {
icon?: string;
// TODO: Add `checkbox` (which overrides any `icon`)
shortcut?: Array<string>;
shortcutRequiresLock?: boolean;
action?: Function;
children?: SectionsOfMenuListEntries;
}
export type MenuListEntry = MenuListEntryData & { ref?: typeof FloatingMenu | typeof MenuList };
const KEYBOARD_LOCK_USE_FULLSCREEN = "This hotkey is reserved by the browser, but becomes available in fullscreen mode";
const KEYBOARD_LOCK_SWITCH_BROWSER = "This hotkey is reserved by the browser, but becomes available in Chrome, Edge, and Opera which support the Keyboard.lock() API";
const MenuList = defineComponent({
inject: ["fullscreen"],
props: {
direction: { type: String as PropType<MenuDirection>, default: MenuDirection.Bottom },
menuEntries: { type: Array as PropType<SectionsOfMenuListEntries>, required: true },
@ -242,6 +249,7 @@ const MenuList = defineComponent({
data() {
return {
currentEntry: this.activeEntry,
keyboardLockInfoMessage: keyboardLockApiSupported() ? KEYBOARD_LOCK_USE_FULLSCREEN : KEYBOARD_LOCK_SWITCH_BROWSER,
SeparatorDirection,
SeparatorType,
MenuDirection,

View File

@ -65,13 +65,13 @@ const menuEntries: MenuListEntries = [
ref: undefined,
children: [
[
{ label: "New", icon: "File", shortcut: ["Ctrl", "N"], action: async () => (await wasm).new_document() },
{ label: "New", icon: "File", shortcut: ["Ctrl", "N"], shortcutRequiresLock: true, action: async () => (await wasm).new_document() },
{ label: "Open…", shortcut: ["Ctrl", "O"] },
{
label: "Open Recent",
shortcut: ["Ctrl", "⇧", "O"],
children: [
[{ label: "Reopen Last Closed", shortcut: ["Ctrl", "⇧", "T"] }, { label: "Clear Recently Opened" }],
[{ label: "Reopen Last Closed", shortcut: ["Ctrl", "⇧", "T"], shortcutRequiresLock: true }, { label: "Clear Recently Opened" }],
[
{ label: "Some Recent File.gdd" },
{ label: "Another Recent File.gdd" },
@ -83,7 +83,7 @@ const menuEntries: MenuListEntries = [
},
],
[
{ label: "Close", shortcut: ["Ctrl", "W"], action: async () => (await wasm).close_active_document_with_confirmation() },
{ label: "Close", shortcut: ["Ctrl", "W"], shortcutRequiresLock: true, action: async () => (await wasm).close_active_document_with_confirmation() },
{ label: "Close All", shortcut: ["Ctrl", "Alt", "W"], action: async () => (await wasm).close_all_documents_with_confirmation() },
],
[

View File

@ -86,11 +86,14 @@ import Link from "../../../../assets/12px-solid/link.svg";
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 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";
import VerticalEllipsis from "../../../../assets/12px-solid/vertical-ellipsis.svg";
import CloseX from "../../../../assets/12px-solid/close-x.svg";
import FullscreenEnter from "../../../../assets/12px-solid/fullscreen-enter.svg";
import FullscreenExit from "../../../../assets/12px-solid/fullscreen-exit.svg";
import WindowButtonWinMinimize from "../../../../assets/12px-solid/window-button-win-minimize.svg";
import WindowButtonWinMaximize from "../../../../assets/12px-solid/window-button-win-maximize.svg";
import WindowButtonWinRestoreDown from "../../../../assets/12px-solid/window-button-win-restore-down.svg";
@ -164,11 +167,14 @@ const icons = {
Grid: { component: Grid, size: 12 },
Overlays: { component: Overlays, size: 12 },
Snapping: { component: Snapping, size: 12 },
Info: { component: Info, size: 12 },
Swap: { component: Swap, size: 12 },
ResetColors: { component: ResetColors, size: 12 },
DropdownArrow: { component: DropdownArrow, size: 12 },
VerticalEllipsis: { component: VerticalEllipsis, size: 12 },
CloseX: { component: CloseX, size: 12 },
FullscreenEnter: { component: FullscreenEnter, size: 12 },
FullscreenExit: { component: FullscreenExit, size: 12 },
WindowButtonWinMinimize: { component: WindowButtonWinMinimize, size: 12 },
WindowButtonWinMaximize: { component: WindowButtonWinMaximize, size: 12 },
WindowButtonWinRestoreDown: { component: WindowButtonWinRestoreDown, size: 12 },

View File

@ -59,7 +59,7 @@ export default defineComponent({
},
data() {
return {
platform: ApplicationPlatform.Windows,
platform: ApplicationPlatform.Web,
maximized: true,
};
},

View File

@ -8,6 +8,7 @@
</div>
<div class="header-third">
<WindowButtonsWindows :maximized="maximized" v-if="platform === ApplicationPlatform.Windows || platform === ApplicationPlatform.Linux" />
<WindowButtonsWeb :maximized="maximized" v-if="platform === ApplicationPlatform.Web" />
</div>
</template>
@ -36,15 +37,10 @@ import MenuBarInput from "../../widgets/inputs/MenuBarInput.vue";
import WindowTitle from "./WindowTitle.vue";
import WindowButtonsWindows from "./WindowButtonsWindows.vue";
import WindowButtonsMac from "./WindowButtonsMac.vue";
import WindowButtonsWeb from "./WindowButtonsWeb.vue";
import { ApplicationPlatform } from "../MainWindow.vue";
export default defineComponent({
components: {
MenuBarInput,
WindowTitle,
WindowButtonsWindows,
WindowButtonsMac,
},
props: {
platform: { type: String, required: true },
maximized: { type: Boolean, required: true },
@ -54,5 +50,12 @@ export default defineComponent({
ApplicationPlatform,
};
},
components: {
MenuBarInput,
WindowTitle,
WindowButtonsWindows,
WindowButtonsMac,
WindowButtonsWeb,
},
});
</script>

View File

@ -20,15 +20,15 @@
border-radius: 50%;
&.close {
background: #ff5f57;
background: #ff5a52;
}
&.minimize {
background: #ffbd2f;
background: #e6c029;
}
&.zoom {
background: #29c93f;
background: #54c22b;
}
}
}

View File

@ -0,0 +1,55 @@
<template>
<div class="window-buttons-web" @click="handleClick" :title="fullscreen.windowFullscreen ? 'Exit Fullscreen (F11)' : 'Enter Fullscreen (F11)'">
<TextLabel v-if="requestFullscreenHotkeys">Click to access all hotkeys</TextLabel>
<Icon :icon="fullscreen.windowFullscreen ? 'FullscreenExit' : 'FullscreenEnter'" />
</div>
</template>
<style lang="scss">
.window-buttons-web {
display: flex;
align-items: center;
padding: 0 8px;
fill: var(--color-e-nearwhite);
.text-label {
font-weight: normal;
font-style: italic;
margin-right: 8px;
}
&:hover {
background: var(--color-6-lowergray);
color: var(--color-f-white);
fill: var(--color-f-white);
}
}
</style>
<script lang="ts">
import { defineComponent } from "vue";
import fullscreen, { keyboardLockApiSupported, enterFullscreen, exitFullscreen } from "@/utilities/fullscreen";
import Icon from "@/components/widgets/labels/Icon.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
const canUseKeyboardLock = keyboardLockApiSupported();
export default defineComponent({
inject: ["fullscreen"],
methods: {
async handleClick() {
if (fullscreen.windowFullscreen) exitFullscreen();
else enterFullscreen();
},
},
computed: {
requestFullscreenHotkeys() {
return canUseKeyboardLock && !fullscreen.keyboardLocked;
},
},
components: {
Icon,
TextLabel,
},
});
</script>

View File

@ -1,9 +1,11 @@
import { createApp } from "vue";
import { fullscreenModeChanged } from "@/utilities/fullscreen";
import { handleKeyUp, handleKeyDown } from "@/utilities/input";
import App from "./App.vue";
// Bind global browser events
document.addEventListener("contextmenu", (e) => e.preventDefault());
document.addEventListener("fullscreenchange", () => fullscreenModeChanged());
window.addEventListener("keyup", (e: KeyboardEvent) => handleKeyUp(e));
window.addEventListener("keydown", (e: KeyboardEvent) => handleKeyDown(e));

View File

@ -0,0 +1,37 @@
import { reactive, readonly } from "vue";
const state = reactive({
windowFullscreen: false,
keyboardLocked: false,
});
export function fullscreenModeChanged() {
state.windowFullscreen = Boolean(document.fullscreenElement);
if (!state.windowFullscreen) state.keyboardLocked = false;
}
export function keyboardLockApiSupported(): boolean {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return "keyboard" in navigator && "lock" in (navigator as any).keyboard;
}
export async function enterFullscreen() {
await document.documentElement.requestFullscreen();
if (keyboardLockApiSupported()) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await (navigator as any).keyboard.lock(["ControlLeft", "ControlRight"]);
state.keyboardLocked = true;
}
}
export async function exitFullscreen() {
await document.exitFullscreen();
}
export async function toggleFullscreen() {
if (state.windowFullscreen) await exitFullscreen();
else await enterFullscreen();
}
export default readonly(state);

View File

@ -1,3 +1,5 @@
import { toggleFullscreen } from "@/utilities/fullscreen";
const wasm = import("@/../wasm/pkg");
export function shouldRedirectKeyboardEventToBackend(e: KeyboardEvent): boolean {
@ -6,7 +8,11 @@ export function shouldRedirectKeyboardEventToBackend(e: KeyboardEvent): boolean
if (target.nodeName === "INPUT" || target.nodeName === "TEXTAREA" || target.isContentEditable) return false;
// Don't redirect a fullscreen request
if (e.key.toLowerCase() === "f11") return false;
if (e.key.toLowerCase() === "f11" && e.type === "keydown" && !e.repeat) {
e.preventDefault();
toggleFullscreen();
return false;
}
// Don't redirect a reload request
if (e.key.toLowerCase() === "f5") return false;