diff --git a/.gitignore b/.gitignore index 29efead2e..bcd7b7cb9 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,6 @@ yarn-error.log* # Editor directories and files .idea -.vscode *.suo *.ntvs* *.njsproj diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..86d085f84 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "vue.volar", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint" + ] +} \ No newline at end of file diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 000000000..40608e163 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,8 @@ +{ + "extensions": [ + ".vue" + ], + "vueCompilerOptions": { + "target": 2 + } +} \ No newline at end of file diff --git a/package.json b/package.json index c4f597752..a0967b67e 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "electron:build": "vue-cli-service electron:build", "electron:serve": "vue-cli-service electron:serve", "postinstall": "electron-builder install-app-deps && patch-package", - "postuninstall": "electron-builder install-app-deps" + "postuninstall": "electron-builder install-app-deps", + "test:e2e": "npx playwright test" }, "main": "background.js", "dependencies": { @@ -32,7 +33,6 @@ "dotenv": "^8.2.0", "electron-updater": "^4.3.1", "fluent-ffmpeg": "^2.1.2", - "font-list": "^1.3.1", "fuse.js": "^6.2.1", "golden-layout": "^1.5.9", "grandiose": "github:vcync/grandiose#feat/workerCompatibility", @@ -70,21 +70,26 @@ "@babel/core": "^7.0.0-0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0", "@babel/plugin-proposal-optional-chaining": "^7.16.0", + "@playwright/test": "^1.31.2", "@semantic-release/git": "^9.0.0", "@vue/cli-plugin-babel": "^4.5.15", "@vue/cli-plugin-eslint": "^3.12.1", "@vue/cli-service": "^3.12.1", "@vue/eslint-config-prettier": "^4.0.1", + "@vue/runtime-dom": "^3.2.47", "babel-eslint": "^10.0.3", "core-js": "^3.19.1", "electron": "^23.1.2", "electron-builder": "^22.9.1", "electron-notarize": "^1.2.2", + "electron-playwright-helpers": "^1.5.3", "eslint": "^5.16.0", "eslint-plugin-no-for-each": "^0.1.14", "eslint-plugin-vue": "^5.2.3", "lint-staged": "^8.2.1", "node-loader": "^0.6.0", + "playwright": "^1.31.2", + "playwright-core": "^1.31.2", "sass-loader": "^7.3.1", "text-loader": "0.0.1", "vue-cli-plugin-electron-builder": "^2.0.0", diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 000000000..691eac234 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,9 @@ +import { defineConfig, expect } from "@playwright/test"; +import extensions from "./tests/e2e/extentions"; + +expect.extend(extensions); + +export default defineConfig({ + testDir: "./tests/e2e/spec", + workers: process.env.CI ? 1 : 2 +}); diff --git a/src/App.vue b/src/App.vue index 1bd2b01c6..cec59cad4 100644 --- a/src/App.vue +++ b/src/App.vue @@ -142,20 +142,11 @@ export default { state: null, layoutState: null, - showUi: true, - mouseTimer: null, - cursor: "none", triggerUiRestart: 0 }; }, computed: { - pluginComponents() { - return this.$modV.store.state.plugins - .filter(plugin => "component" in plugin) - .map(plugin => plugin.component.name); - }, - focusedModules() { const focusedOrPinned = this.$store.getters["ui-modules/focusedOrPinned"]; const modules = focusedOrPinned.map( @@ -191,50 +182,6 @@ export default { }, methods: { - makeFullScreen() { - if (!document.body.ownerDocument.webkitFullscreenElement) { - document.body.webkitRequestFullscreen(); - } else { - document.body.ownerDocument.webkitExitFullscreen(); - } - }, - - mouseMove() { - if (this.mouseTimer) { - clearTimeout(this.mouseTimer); - } - - this.cursor = "default"; - this.mouseTimer = setTimeout(this.movedMouse, 200); - }, - - movedMouse() { - if (this.mouseTimer) { - this.mouseTimer = null; - } - - this.cursor = "none"; - }, - - getProps(moduleName) { - const moduleDefinition = this.$modV.store.state.modules.registered[ - moduleName - ]; - - return Object.keys(moduleDefinition.props).filter( - key => - moduleDefinition.props[key].type === "int" || - moduleDefinition.props[key].type === "float" || - moduleDefinition.props[key].type === "text" || - moduleDefinition.props[key].type === "bool" || - moduleDefinition.props[key].type === "color" || - moduleDefinition.props[key].type === "vec2" || - moduleDefinition.props[key].type === "tween" || - moduleDefinition.props[key].type === "texture" || - moduleDefinition.props[key].type === "enum" - ); - }, - toggleModulePin(id) { if (this.isPinned(id)) { this.$store.commit("ui-modules/REMOVE_PINNED", id); diff --git a/src/application/index.js b/src/application/index.js index bf60b0d89..443a72587 100644 --- a/src/application/index.js +++ b/src/application/index.js @@ -1,3 +1,9 @@ +import PromiseWorker from "promise-worker-transferable"; +import Vue from "vue"; +import { ipcRenderer } from "electron"; +import { app } from "@electron/remote"; +import { createWebcodecVideo } from "./createWebcodecVideo"; + import Worker from "worker-loader!./worker/index.worker.js"; import { setupMedia, @@ -7,16 +13,9 @@ import { } from "./setup-media"; import setupBeatDetektor from "./setup-beat-detektor"; import setupMidi from "./setup-midi"; - import store from "./worker/store"; import windowHandler from "./window-handler"; import use from "./use"; - -import PromiseWorker from "promise-worker-transferable"; -import Vue from "vue"; -import { ipcRenderer } from "electron"; -import { app } from "@electron/remote"; -import { createWebcodecVideo } from "./createWebcodecVideo"; import { GROUP_ENABLED } from "./constants"; let imageBitmap; @@ -158,6 +157,8 @@ export default class ModV { async setup(canvas = document.createElement("canvas")) { this.windowHandler(); + this.enumerateFonts(); + try { await this.setupMedia({ useDefaultDevices: true }); } catch (e) { @@ -310,4 +311,21 @@ export default class ModV { loadPreset(filePathToPreset) { this.$worker.postMessage({ type: "loadPreset", payload: filePathToPreset }); } + + async enumerateFonts() { + const localFonts = await window.queryLocalFonts(); + const fonts = []; + + for (let i = 0; i < localFonts.length; i += 1) { + const { family, fullName, postscriptName, style } = localFonts[i]; + + fonts.push({ family, fullName, postscriptName, style }); + + // No need to await here, async loading is fine. + // The user can't use fonts fonts immediately at this stage, so no need to block the thread + document.fonts.load(`14px ${postscriptName}`, fullName); + } + + this.store.commit("fonts/SET_LOCAL_FONTS", fonts); + } } diff --git a/src/application/utils/apply-expression.js b/src/application/utils/apply-expression.js index 7b6f3d37a..b3cb08566 100644 --- a/src/application/utils/apply-expression.js +++ b/src/application/utils/apply-expression.js @@ -1,3 +1,4 @@ +import get from "lodash.get"; import store from "../worker/store"; export function applyExpression({ value, inputId }) { @@ -5,12 +6,15 @@ export function applyExpression({ value, inputId }) { inputId ); + const input = store.state.inputs.inputs[inputId]; + let dataOut = value; if (expressionAssignment) { const scope = { value: dataOut, - time: Date.now() + time: Date.now(), + inputValue: get(store.state, input.getLocation) }; dataOut = expressionAssignment.func.evaluate(scope); diff --git a/src/application/window-handler.js b/src/application/window-handler.js index ffea8834e..39dfd17b6 100644 --- a/src/application/window-handler.js +++ b/src/application/window-handler.js @@ -1,5 +1,6 @@ export default function windowHandler() { const windows = {}; + const that = this; function createHideMouseTimerhandler(canvas) { let mouseTimer; @@ -18,10 +19,16 @@ export default function windowHandler() { }; } - function configureWindow({ win, canvas, title, backgroundColor }) { + function configureWindow({ win, canvas, backgroundColor }) { win.document.body.appendChild(canvas); - win.document.title = title; win.document.body.style.backgroundColor = backgroundColor; + win.addEventListener("beforeunload", ev => { + // Setting any value other than undefined here will prevent the window + // from closing or reloading + ev.returnValue = true; + }); + + setSize.call(that, win); } function pollToConfigureWindow(args) { @@ -62,6 +69,7 @@ export default function windowHandler() { "modal", `width=${width}, height=${height}, location=no, menubar=no, left=0` ); + win.document.title = title; if (win === null || typeof win === "undefined") { console.log( @@ -120,8 +128,6 @@ export default function windowHandler() { setSize.call(this, win); }); }); - - setSize.call(this, win); } }); diff --git a/src/application/worker/index.worker.js b/src/application/worker/index.worker.js index 96bcd6c36..9b52ead43 100644 --- a/src/application/worker/index.worker.js +++ b/src/application/worker/index.worker.js @@ -11,6 +11,9 @@ async function start() { const grabCanvasPlugin = require("../plugins/grab-canvas").default; const get = require("lodash.get"); + // For Playwright + self._get = get; + const { tick: frameTick } = require("./frame-counter"); const { getFeatures, setFeatures } = require("./audio-features"); // const featureAssignmentPlugin = require("../plugins/feature-assignment"); @@ -344,6 +347,7 @@ async function start() { store.commit("groups/SWAP"); store.commit("modules/SWAP"); store.commit("inputs/SWAP"); + store.commit("expressions/SWAP"); return; } diff --git a/src/application/worker/store/modules/common/swap.js b/src/application/worker/store/modules/common/swap.js index 667aa3674..9eed32aa5 100644 --- a/src/application/worker/store/modules/common/swap.js +++ b/src/application/worker/store/modules/common/swap.js @@ -7,7 +7,7 @@ import Vue from "vue"; * The idea is that this makes loading presets smooth and the end user will not see any * glitches in the render loop. */ -export default function SWAP(swap, getDefault, sharedPropertyRestrictions) { +export function SWAP(swap, getDefault, sharedPropertyRestrictions) { return function(state) { const stateKeys = Object.keys(state); @@ -82,8 +82,6 @@ export default function SWAP(swap, getDefault, sharedPropertyRestrictions) { } } }); - } else { - Object.assign(swap, getDefault()); } Object.assign(swap, getDefault()); diff --git a/src/application/worker/store/modules/dataTypes.js b/src/application/worker/store/modules/dataTypes.js index aeb98e577..7938e5360 100644 --- a/src/application/worker/store/modules/dataTypes.js +++ b/src/application/worker/store/modules/dataTypes.js @@ -2,6 +2,9 @@ import store from "../"; import { frames, advanceFrame } from "./tweens"; const state = { + text: { + get: value => value + }, int: { get: value => value }, @@ -122,6 +125,10 @@ const state = { } }; +const getters = { + types: state => Object.keys(state) +}; + const actions = { createType({ state }, { type, args }) { return state[type].create(args); @@ -131,5 +138,6 @@ const actions = { export default { namespaced: true, state, + getters, actions }; diff --git a/src/application/worker/store/modules/expressions.js b/src/application/worker/store/modules/expressions.js index 6bfc2fbe3..cfca79b47 100644 --- a/src/application/worker/store/modules/expressions.js +++ b/src/application/worker/store/modules/expressions.js @@ -1,9 +1,16 @@ +import get from "lodash.get"; import { v4 as uuidv4 } from "uuid"; +import { SWAP } from "./common/swap"; const math = require("mathjs"); -const state = { - assignments: {} -}; +function getDefaultState() { + return { + assignments: {} + }; +} + +const state = getDefaultState(); +const swap = getDefaultState(); // getters const getters = { @@ -14,8 +21,8 @@ const getters = { } }; -function compileExpression(expression) { - const scope = { value: 0, time: 0 }; +function compileExpression(expression, scopeItems = {}) { + const scope = { value: 0, time: 0, ...scopeItems }; let newFunction; try { @@ -32,7 +39,10 @@ function compileExpression(expression) { // actions const actions = { - create({ commit }, { expression = "value", id, inputId }) { + create( + { rootState, commit }, + { expression = "value", id, inputId, writeToSwap } + ) { if (!inputId) { throw new Error("Input ID required"); } @@ -43,7 +53,15 @@ const actions = { const expressionId = id || uuidv4(); - const func = compileExpression(expression); + const input = rootState.inputs.inputs[inputId]; + + const func = compileExpression(expression, { + // We currrently have no way of interacting with swap state. + // This would be something to fix in the future, maybe use an entire store + // for swap, or write a more specific mechanism to look up values in swap + // state. + inputValue: writeToSwap ? 0 : get(rootState, input.getLocation) + }); if (!func) { throw new Error("Unable to compile Expression"); @@ -56,12 +74,12 @@ const actions = { expression }; - commit("ADD_EXPRESSION", { assignment }); + commit("ADD_EXPRESSION", { assignment, writeToSwap }); return expressionId; }, - update({ commit }, { id, expression = "value" }) { + update({ rootState, commit }, { id, expression = "value", writeToSwap }) { if (!id) { throw new Error("Expression ID required"); } @@ -77,7 +95,11 @@ const actions = { return null; } - const func = compileExpression(expression); + const input = rootState.inputs.inputs[existingExpression.inputId]; + + const func = compileExpression(expression, { + inputValue: get(rootState, input.getLocation) + }); if (!func) { throw new Error("Unable to compile Expression"); @@ -86,7 +108,7 @@ const actions = { existingExpression.func = func; existingExpression.expression = expression; - commit("ADD_EXPRESSION", { assignment: existingExpression }); + commit("ADD_EXPRESSION", { assignment: existingExpression, writeToSwap }); return existingExpression.id; }, @@ -103,20 +125,23 @@ const actions = { for (let i = 0, len = assignments.length; i < len; i++) { const assignment = assignments[i]; - await dispatch("create", assignment); + await dispatch("create", { ...assignment, writeToSwap: true }); } } }; // mutations const mutations = { - ADD_EXPRESSION(state, { assignment }) { - state.assignments[assignment.id] = assignment; + ADD_EXPRESSION(state, { assignment, writeToSwap = false }) { + const writeTo = writeToSwap ? swap : state; + writeTo.assignments[assignment.id] = assignment; }, REMOVE_EXPRESSION(state, { id }) { delete state.assignments[id]; - } + }, + + SWAP: SWAP(swap, getDefaultState) }; export default { diff --git a/src/application/worker/store/modules/fonts.js b/src/application/worker/store/modules/fonts.js new file mode 100644 index 000000000..0b2079d14 --- /dev/null +++ b/src/application/worker/store/modules/fonts.js @@ -0,0 +1,30 @@ +const state = { + defaultFonts: ["serif", "sans-serif", "cursive", "monospace"], + localFonts: [] +}; + +const getters = { + fonts: state => [ + ...state.defaultFonts, + ...state.localFonts + .filter( + (value, index, self) => + index === self.findIndex(t => t.family === value.family) + ) + .map(font => font.family) + .sort((a, b) => a.localeCompare(b)) + ] +}; + +const mutations = { + SET_LOCAL_FONTS(state, fonts = []) { + state.localFonts = fonts; + } +}; + +export default { + namespaced: true, + state, + getters, + mutations +}; diff --git a/src/application/worker/store/modules/groups.js b/src/application/worker/store/modules/groups.js index db7698a62..d480ecb29 100644 --- a/src/application/worker/store/modules/groups.js +++ b/src/application/worker/store/modules/groups.js @@ -1,4 +1,4 @@ -import SWAP from "./common/swap"; +import { SWAP } from "./common/swap"; import store from "../"; import constants, { GROUP_DISABLED } from "../../../constants"; import { v4 as uuidv4 } from "uuid"; diff --git a/src/application/worker/store/modules/inputs.js b/src/application/worker/store/modules/inputs.js index 62ebf43e8..bc4dd8fed 100644 --- a/src/application/worker/store/modules/inputs.js +++ b/src/application/worker/store/modules/inputs.js @@ -1,6 +1,6 @@ import Vue from "vue"; import { v4 as uuidv4 } from "uuid"; -import SWAP from "./common/swap"; +import { SWAP } from "./common/swap"; /** * InputLinkType enum string values. @@ -99,8 +99,11 @@ const actions = { commit("SET_FOCUSED_INPUT", { id: null, title: null }); }, - addInput({ commit }, { type, location, data, id = uuidv4(), writeToSwap }) { - const input = { type, location, data, id }; + addInput( + { commit }, + { type, getLocation, location, data, id = uuidv4(), writeToSwap } + ) { + const input = { type, getLocation, location, data, id }; commit("ADD_INPUT", { input, writeToSwap }); return input; }, diff --git a/src/application/worker/store/modules/modules.js b/src/application/worker/store/modules/modules.js index a5c30a66e..580ed463d 100644 --- a/src/application/worker/store/modules/modules.js +++ b/src/application/worker/store/modules/modules.js @@ -1,5 +1,5 @@ import Vue from "vue"; -import SWAP from "./common/swap"; +import { SWAP } from "./common/swap"; import getNextName from "../../../utils/get-next-name"; import getPropDefault from "../../../utils/get-prop-default"; import store from ".."; @@ -80,6 +80,7 @@ async function initialiseModuleProperties( ) { const inputBind = await store.dispatch("inputs/addInput", { type: "action", + getLocation: `modules.active["${module.$id}"].props["${propKey}"]`, location: "modules/updateProp", data: { moduleId: module.$id, prop: propKey }, writeToSwap @@ -96,6 +97,7 @@ async function initialiseModuleProperties( const key = dataTypeInputsKeys[i]; await store.dispatch("inputs/addInput", { type: "action", + getLocation: `modules.active["${module.$id}"].props["${propKey}"]["${key}"]`, location: "modules/updateProp", data: { moduleId: module.$id, @@ -288,6 +290,7 @@ const actions = { if (!moduleMeta.isGallery) { const alphaInputBind = await store.dispatch("inputs/addInput", { type: "action", + getLocation: `modules.active["${module.$id}"].meta.alpha`, location: "modules/updateMeta", data: { id: module.$id, metaKey: "alpha" } }); @@ -296,6 +299,7 @@ const actions = { const enabledInputBind = await store.dispatch("inputs/addInput", { type: "action", + getLocation: `modules.active["${module.$id}"].meta.enabled`, location: "modules/updateMeta", data: { id: module.$id, metaKey: "enabled" } }); @@ -304,6 +308,7 @@ const actions = { const coInputBind = await store.dispatch("inputs/addInput", { type: "action", + getLocation: `modules.active["${module.$id}"].meta.compositeOperation`, location: "modules/updateMeta", data: { moduleId: module.$id, metaKey: "compositeOperation" } }); diff --git a/src/application/worker/store/modules/videos.js b/src/application/worker/store/modules/videos.js index 8dfbcc87c..a359531ab 100644 --- a/src/application/worker/store/modules/videos.js +++ b/src/application/worker/store/modules/videos.js @@ -31,7 +31,7 @@ const actions = { }); } - commit("CREATE_VIDEO", { id, path }); + commit("CREATE_VIDEO", { id, path: filePath }); return { id }; }, diff --git a/src/background/background.js b/src/background/background.js index 72b795ba5..592a2562c 100644 --- a/src/background/background.js +++ b/src/background/background.js @@ -1,4 +1,4 @@ -import { app, protocol } from "electron"; +import { app, ipcMain, protocol } from "electron"; import { APP_SCHEME } from "./background-constants"; import { getMediaManager } from "./media-manager"; import { openFile } from "./open-file"; @@ -37,7 +37,7 @@ app.on("window-all-closed", () => { app.on("activate", async () => { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. - createWindow("mainWindow"); + createWindow({ windowName: "mainWindow" }); }); // https://stackoverflow.com/a/66673831 @@ -73,8 +73,10 @@ app.on("ready", async () => { ); createWindow({ windowName: "mainWindow" }); - createWindow({ windowName: "splashScreen" }); - createWindow({ windowName: "colorPicker", options: { show: false } }); + ipcMain.once("main-window-created", () => { + createWindow({ windowName: "splashScreen" }); + createWindow({ windowName: "colorPicker", options: { show: false } }); + }); }); // Exit cleanly on request from parent process in development mode. diff --git a/src/background/menu-bar.js b/src/background/menu-bar.js index f457caf08..f42529193 100644 --- a/src/background/menu-bar.js +++ b/src/background/menu-bar.js @@ -1,4 +1,5 @@ import fs from "fs"; +import path from "path"; import { dialog, shell, app, ipcMain, Menu } from "electron"; import { windows } from "./windows"; import { openFile } from "./open-file"; @@ -7,6 +8,54 @@ import { projectNames, setCurrentProject, currentProject } from "./projects"; const isMac = process.platform === "darwin"; +let lastFileSavedPath = null; + +async function save(filePath) { + let result; + if (!filePath) { + result = await dialog.showSaveDialog(windows["mainWindow"], { + defaultPath: lastFileSavedPath || "preset.json", + filters: [{ name: "Presets", extensions: ["json"] }] + }); + + if (result.canceled) { + return; + } + } + + try { + await writePresetToFile(filePath ?? result.filePath); + lastFileSavedPath = path.resolve(filePath ?? result.filePath); + updateMenu(); + } catch (e) { + console.error(e); + } +} + +async function writePresetToFile(filePath) { + ipcMain.once("preset-data", async (_, presetData) => { + try { + await fs.promises.writeFile(filePath, presetData); + } catch (e) { + dialog.showMessageBox(windows["mainWindow"], { + type: "error", + message: "Could not save preset to file", + detail: e.toString() + }); + } + }); + + try { + windows["mainWindow"].webContents.send("generate-preset"); + } catch (e) { + dialog.showMessageBox(windows["mainWindow"], { + type: "error", + message: "Could not generate preset", + detail: e.toString() + }); + } +} + export function generateMenuTemplate() { const mediaManager = getMediaManager(); @@ -70,38 +119,16 @@ export function generateMenuTemplate() { { type: "separator" }, { label: "Save Preset", + accelerator: "CmdOrCtrl+S", + async click() { + save(lastFileSavedPath); + } + }, + { + label: "Save Preset As…", accelerator: "CmdOrCtrl+Shift+S", async click() { - const result = await dialog.showSaveDialog(windows["mainWindow"], { - defaultPath: "preset.json", - filters: [{ name: "Presets", extensions: ["json"] }] - }); - - if (result.canceled) { - return; - } - - ipcMain.once("preset-data", async (event, presetData) => { - try { - await fs.promises.writeFile(result.filePath, presetData); - } catch (e) { - dialog.showMessageBox(windows["mainWindow"], { - type: "error", - message: "Could not save preset to file", - detail: e.toString() - }); - } - }); - - try { - windows["mainWindow"].webContents.send("generate-preset"); - } catch (e) { - dialog.showMessageBox(windows["mainWindow"], { - type: "error", - message: "Could not generate preset", - detail: e.toString() - }); - } + save(); } }, @@ -239,7 +266,14 @@ export function generateMenuTemplate() { ]; } -export function updateMenu() { +export function updateMenu(setWindowListener) { + if (setWindowListener) { + windows["mainWindow"].on("ready-to-show", () => { + lastFileSavedPath = null; + updateMenu(); + }); + } + const menu = Menu.buildFromTemplate(generateMenuTemplate()); Menu.setApplicationMenu(menu); } diff --git a/src/background/window-prefs.js b/src/background/window-prefs.js index 43af0ed75..abed2ec64 100644 --- a/src/background/window-prefs.js +++ b/src/background/window-prefs.js @@ -8,8 +8,10 @@ import { closeWindow, createWindow, windows } from "./windows"; import { updateMenu } from "./menu-bar"; import { getMediaManager } from "./media-manager"; -const isDevelopment = process.env.NODE_ENV !== "production"; +const isDevelopment = process.env.NODE_ENV === "development"; +const isTest = process.env.CI === "e2e"; let shouldCloseMainWindowAndQuit = false; +let modVReady = false; const windowPrefs = { colorPicker: { @@ -65,6 +67,8 @@ const windowPrefs = { beforeCreate() { const { width, height } = screen.getPrimaryDisplay().workAreaSize; + modVReady = false; + return { options: { width, @@ -76,6 +80,8 @@ const windowPrefs = { async create(window) { require("@electron/remote/main").enable(window.webContents); + ipcMain.handle("is-modv-ready", () => modVReady); + // Configure child windows to open without a menubar (windows/linux) window.webContents.on( "new-window", @@ -84,7 +90,10 @@ const windowPrefs = { event.preventDefault(); event.newGuest = new BrowserWindow({ ...options, - autoHideMenuBar: true + autoHideMenuBar: true, + closable: false, + enableLargerThanScreen: true, + title: "" }); event.newGuest.removeMenu(); @@ -114,6 +123,7 @@ const windowPrefs = { }); ipcMain.on("modv-ready", () => { + modVReady = true; mm.start(); }); @@ -144,7 +154,7 @@ const windowPrefs = { window.webContents.send("input-update", message); }); - if (!isDevelopment || process.env.IS_TEST) { + if (!isDevelopment && !isTest) { window.on("close", async e => { if (shouldCloseMainWindowAndQuit) { app.quit(); @@ -211,16 +221,13 @@ const windowPrefs = { unique: true, async create(window) { - windows["mainWindow"].maximize(); - ipcMain.on("modv-ready", () => { try { window.close(); } catch (e) { console.error(e); } - - windows["mainWindow"].show(); + windows["mainWindow"].maximize(); }); } } diff --git a/src/background/windows.js b/src/background/windows.js index 7ea6f9e07..d23e911e0 100644 --- a/src/background/windows.js +++ b/src/background/windows.js @@ -7,8 +7,6 @@ import { windowPrefs } from "./window-prefs"; const windows = {}; function createWindow({ windowName, options = {} }, event) { - updateMenu(); - if (windowPrefs[windowName].unique && windows[windowName]) { windows[windowName].focus(); windows[windowName].show(); @@ -36,6 +34,8 @@ function createWindow({ windowName, options = {} }, event) { ...options }); + updateMenu(true); + if (typeof windowPrefs[windowName].create === "function") { windowPrefs[windowName].create(windows[windowName]); } diff --git a/src/components/ActiveModule.vue b/src/components/ActiveModule.vue index 0a1adc1b6..b050e7cb3 100644 --- a/src/components/ActiveModule.vue +++ b/src/components/ActiveModule.vue @@ -6,8 +6,13 @@ @focus="clickActiveModule" ref="activeModule" :class="{ focused }" + :id="`active-module-${id}`" > -
+
{{ name }} - moduleDefinition.props[key].type === "int" || - moduleDefinition.props[key].type === "float" || - moduleDefinition.props[key].type === "text" || - moduleDefinition.props[key].type === "bool" || - moduleDefinition.props[key].type === "color" || - moduleDefinition.props[key].type === "vec2" || - moduleDefinition.props[key].type === "tween" || - moduleDefinition.props[key].type === "texture" || - moduleDefinition.props[key].type === "enum" - ); - }, - focusInput(id, title) { this.$modV.store.dispatch("inputs/setFocusedInput", { id, @@ -246,6 +238,16 @@ export default { this.$emit("remove-module", this.id); } + }, + + titleMouseDown() { + this.grabbing = true; + window.addEventListener("mouseup", this.titleMouseUp); + }, + + titleMouseUp() { + this.grabbing = false; + window.removeEventListener("mouseup", this.titleMouseUp); } } }; @@ -263,12 +265,17 @@ export default { outline: #c4c4c4 2px solid; } -.active-module__title { +.active-module__name { background: #9a9a9a; width: 100%; overflow: hidden; text-overflow: ellipsis; position: relative; + cursor: grab; +} + +.active-module__name.grabbing { + cursor: grabbing; } .active-module__controls, @@ -279,7 +286,7 @@ export default { } .active-module__controls grid, -.active-module__title { +.active-module__name { box-sizing: border-box; padding: 0 4px; } diff --git a/src/components/Control.vue b/src/components/Control.vue index f9aa80f31..614446126 100644 --- a/src/components/Control.vue +++ b/src/components/Control.vue @@ -3,6 +3,7 @@ columns="4" @mousedown="focusInput" :class="{ 'has-link': hasLink, focused: inputIsFocused }" + :id="`module-control-${inputId}`" > @@ -67,7 +68,7 @@
- - +
@@ -93,6 +94,7 @@ import Vec3Control from "./Controls/Vec3Control"; import Vec4Control from "./Controls/Vec4Control"; import hasLink from "./mixins/has-input-link"; import inputIsFocused from "./mixins/input-is-focused"; +import Select from "./inputs/Select.vue"; export default { mixins: [hasLink, inputIsFocused], @@ -137,7 +139,8 @@ export default { FontControl, ColorControl, Vec3Control, - Vec4Control + Vec4Control, + Select }, data() { diff --git a/src/components/Controls/FontControl.vue b/src/components/Controls/FontControl.vue index 91ca018fd..6f889d667 100644 --- a/src/components/Controls/FontControl.vue +++ b/src/components/Controls/FontControl.vue @@ -11,7 +11,7 @@ :style="{ fontFamily: value }" /> -