From a39dcf87647bfef0360c66c8104d9f13a354eaf9 Mon Sep 17 00:00:00 2001 From: Tony Atkins Date: Fri, 8 Dec 2023 13:43:39 +0100 Subject: [PATCH] GH-59 GH-103 GH-106 GH-108 GH-109 GH-115: Refactored configuration interface and reworked binding structure and execution. --- src/css/common.css | 4 + src/css/settings.css | 269 ++++++- src/html/configuration-panel.html | 43 -- src/html/settings.html | 17 +- src/images/options-icon.svg | 12 + src/js/background.js | 20 +- .../configuration_panel/button-listeners.js | 330 --------- .../configuration-panel.js | 357 ---------- .../configuration_panel/create-panel-utils.js | 156 ----- src/js/configuration_panel/panel-events.js | 173 ----- src/js/content_scripts/action-launcher.js | 153 +--- src/js/content_scripts/actions.js | 227 ++++++ src/js/content_scripts/bindings.js | 38 +- src/js/content_scripts/gamepad-navigator.js | 69 +- .../input-mapper-background-utils.js | 63 +- src/js/content_scripts/input-mapper-base.js | 212 +++--- .../input-mapper-content-utils.js | 657 ++++++++---------- src/js/content_scripts/input-mapper.js | 127 ++-- src/js/content_scripts/search-keyboard.js | 2 +- src/js/settings/addBinding.js | 128 ++++ src/js/settings/bindingsPanels.js | 312 +++++++++ src/js/settings/draftHandlingButton.js | 42 ++ src/js/settings/editBinding.js | 524 ++++++++++++++ src/js/settings/editableSection.js | 72 ++ src/js/settings/prefsPanel.js | 145 ++++ src/js/settings/rangeInput.js | 155 +++++ src/js/settings/selectInput.js | 68 ++ src/js/settings/settings.js | 441 ------------ src/js/settings/settingsMainPanel.js | 133 ++++ src/js/settings/textInput.js | 53 ++ src/js/settings/toggle.js | 2 +- src/js/shared/configuration-maps.js | 183 ----- src/js/shared/prefs.js | 1 + src/js/{content_scripts => shared}/utils.js | 0 src/manifest.json | 6 +- tests/html/select-input.html | 74 ++ tests/html/text-input.html | 2 +- 37 files changed, 2764 insertions(+), 2506 deletions(-) delete mode 100644 src/html/configuration-panel.html create mode 100644 src/images/options-icon.svg delete mode 100644 src/js/configuration_panel/button-listeners.js delete mode 100644 src/js/configuration_panel/configuration-panel.js delete mode 100644 src/js/configuration_panel/create-panel-utils.js delete mode 100644 src/js/configuration_panel/panel-events.js create mode 100644 src/js/content_scripts/actions.js create mode 100644 src/js/settings/addBinding.js create mode 100644 src/js/settings/bindingsPanels.js create mode 100644 src/js/settings/draftHandlingButton.js create mode 100644 src/js/settings/editBinding.js create mode 100644 src/js/settings/editableSection.js create mode 100644 src/js/settings/prefsPanel.js create mode 100644 src/js/settings/rangeInput.js create mode 100644 src/js/settings/selectInput.js delete mode 100644 src/js/settings/settings.js create mode 100644 src/js/settings/settingsMainPanel.js create mode 100644 src/js/settings/textInput.js delete mode 100644 src/js/shared/configuration-maps.js rename src/js/{content_scripts => shared}/utils.js (100%) create mode 100644 tests/html/select-input.html diff --git a/src/css/common.css b/src/css/common.css index c0bb9bb..a052358 100644 --- a/src/css/common.css +++ b/src/css/common.css @@ -6,3 +6,7 @@ * { font-family: ubuntu, sans-serif; } + +.hidden { + display: none; +} diff --git a/src/css/settings.css b/src/css/settings.css index 14aee0f..0fdb40e 100644 --- a/src/css/settings.css +++ b/src/css/settings.css @@ -18,27 +18,43 @@ margin-left: 3rem; } -.gamepad-config-editable-section h3 { +.gamepad-settings-editable-section h3 { font-size: 1.6rem; } -.gamepad-config-editable-section-body { +.gamepad-settings-editable-section-body { display: flex; flex-direction: column; - row-gap: 0.5rem; + font-size: 1.2rem; + margin-left: 2rem; +} + +.gamepad-settings-prefs-panel .gamepad-settings-editable-section-body { + column-gap: 5rem; + display: grid; + grid-template-columns: 1fr; + row-gap: 2rem; } .gamepad-toggle-outer-container { - align-items: center; + column-gap: 1rem; display: flex; flex-direction: row; - margin-left: 2rem; } -.gamepad-toggle-header { +.gamepad-toggle-header, .gamepad-range-input-header, .gamepad-text-input-header, .gamepad-settings-params-select-header { font-size: 1.2rem; font-weight: bold; - width: 75%; +} + +.gamepad-settings-binding-header .gamepad-toggle-header, .gamepad-settings-binding-header .gamepad-range-input-header, .gamepad-settings-binding-header .gamepad-text-input-header { + margin: auto; +} + +.gamepad-toggle-body { + display: flex; + flex-direction: column; + row-gap: 0.5rem; } .gamepad-toggle { @@ -47,11 +63,15 @@ border-radius: 1.25rem; display: flex; height: 2.5rem; - justify-items: left; - margin-left: 0.5rem; + justify-content: left; width: 5rem; } +.gamepad-toggle-description { + font-style: italic; + justify-content: center; +} + .gamepad-toggle:focus { border: 5px solid black; outline: none; @@ -76,7 +96,7 @@ margin-right: 0.25rem; } -.gamepad-config-editable-section-footer { +.gamepad-settings-editable-section-footer { column-gap: 1rem; display: flex; flex-direction: row; @@ -90,3 +110,232 @@ font-weight: bold; height: 3rem; } + +.gamepad-range-input-outer-container { + column-gap: 1rem; + display: flex; + flex-direction: row; +} + +.gamepad-range-input-vertical-container { + display: flex; + flex-direction: column; + width: 100%; +} + +.gamepad-range-input-container { + align-items: center; + display: flex; + flex-direction: row; +} + +.gamepad-range-input { + padding-bottom: 1rem; + padding-top: 1rem; + width: 100%; +} + +.gamepad-range-summary { + margin: auto; +} + +.gamepad-text-input-outer-container { + column-gap: 0.5rem; + display: flex; + flex-direction: row; +} + +.gamepad-text-input-container { + display: flex; + flex-direction: column; + row-gap: 0.5rem; +} + +.gamepad-text-input-description { + font-style: italic; + justify-content: center; +} + +.gamepad-text-input { + font-size: 1.5rem; + height: 2rem; +} + +.gamepad-select-input-container { + display: flex; + flex-direction: column; + font-size: 1.2rem; + row-gap: 1rem; +} + +.gamepad-select-input-label { + font-weight: bold; +} + +.gamepad-select-input-input { + font-size: 1.5rem; + height: 3rem; +} + +.gamepad-settings-binding-params .gamepad-select-input-input { + width: 90%; +} + +.dynamic-binding-components { + display: flex; + flex-direction: column; +} + +.gamepad-settings-binding { + border-top: 1px solid #999; + column-gap: 2rem; + display: flex; + flex-direction: column; + row-gap: 1rem; +} + +.gamepad-settings-binding:first-of-type { + border-top: none; + padding-top: none; +} + +.gamepad-settings-binding-header { + align-items: center; + column-gap: 1rem; + display: grid; + grid-template-columns: 1fr 5rem 15rem; + padding-bottom: 1rem; + padding-top: 1rem; +} + +.gamepad-settings-binding-header .gamepad-select-input-label { + margin: auto; +} + +.gamepad-settings-binding-header .gamepad-select-input-container { + column-gap: 1rem; + display: flex; + flex-direction: row; +} + +button.gamepad-settings-params-icon { + background: none; + border: none; + border-radius: 50%; + height: 3rem; + width: 3rem; +} + +button.gamepad-settings-params-icon:focus:not([disabled]), +button.gamepad-settings-params-icon:hover:not([disabled]) { + outline: none; +} + +button.gamepad-settings-params-icon:focus:not([disabled]) svg path, +button.gamepad-settings-params-icon:hover:not([disabled]) svg path { + fill: blue; + stroke: blue; +} + +.gamepad-settings-params-icon svg { + height: 2.5rem; + width: 2.5rem; +} + +.gamepad-settings-params-icon svg path { + fill: black; + stroke: black; +} + +.gamepad-settings-params-icon[disabled] svg path { + fill: #999; + stroke: #999; +} + +.gamepad-settings-binding-params { + background-color: #eee; + border-top: 1px dashed #666; + column-gap: 5rem; + display: grid; + grid-template-columns: 1fr; + padding-bottom: 1rem; + padding-left: 2rem; + padding-top: 1rem; + row-gap: 2.5rem; +} + +.gamepad-settings-binding-params.hidden { + display: none; +} + +.gamepad-settings-add-binding-addButton, .gamepad-settings-binding-removeButton { + font-size: 1.2rem; + font-weight: bold; + height: 3rem; +} + +.gamepad-settings-binding-removeButton { + align-self: flex-start; +} + +.gamepad-settings-params-select-input-container { + column-gap: 1rem; + display: flex; + flex-direction: row; +} + +.gamepad-settings-binding-header .gamepad-settings-params-select-input-container { + align-items: center; +} + +.gamepad-settings-binding-description { + font-size: 1.2rem; + font-weight: bold; +} + +.gamepad-settings-params-select-body { + display: flex; + flex-direction: column; + row-gap: 0.5rem; +} + +.gamepad-settings-add-binding-container { + align-items: center; + border-bottom: 1px solid #999; + column-gap: 1rem; + display: flex; + flex-direction: row; + font-size: 1.2rem; + font-weight: bold; + padding-bottom: 1rem; +} + +.gamepad-settings-add-binding-container.hidden { + display: none; +} + +@media (min-width: 600px) { + .gamepad-settings-prefs-panel .gamepad-settings-editable-section-body { + grid-template-columns: 1fr 1fr; + } + + .gamepad-settings-binding-params { + grid-template-columns: 1fr 1fr; + } +} + +@media (prefers-color-scheme: dark) { + body { + background-color: black; + color: white; + } + + .gamepad-settings-params-icon svg path { + fill: white; + stroke: white; + } + + .gamepad-settings-binding-params { + background-color: #333; + } +} diff --git a/src/html/configuration-panel.html b/src/html/configuration-panel.html deleted file mode 100644 index 4395d54..0000000 --- a/src/html/configuration-panel.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - -
-
- - -
Loading...
-
- - - - -
-
- - diff --git a/src/html/settings.html b/src/html/settings.html index 39f5af2..06d6e44 100644 --- a/src/html/settings.html +++ b/src/html/settings.html @@ -14,6 +14,7 @@ Gamepad Navigator Settings + @@ -27,9 +28,9 @@ - - + + @@ -45,7 +46,17 @@ - + + + + + + + + + + +
diff --git a/src/images/options-icon.svg b/src/images/options-icon.svg new file mode 100644 index 0000000..cacab49 --- /dev/null +++ b/src/images/options-icon.svg @@ -0,0 +1,12 @@ + + + + diff --git a/src/js/background.js b/src/js/background.js index 98a81db..d118b2a 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -22,11 +22,11 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Open a new tab in the current window. * * @param {Boolean} active - Whether the new tab should be focused when created. - * @param {String} homepageURL - The URL for the new tab. + * @param {String} newTabOrWindowURL - The URL for the new tab. * */ - gamepad.messageListenerUtils.openNewTab = function (active, homepageURL) { - chrome.tabs.create({ active: active, url: homepageURL }); + gamepad.messageListenerUtils.openNewTab = function (active, newTabOrWindowURL) { + chrome.tabs.create({ active: active, url: newTabOrWindowURL }); }; /** @@ -78,12 +78,12 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Open a new window. * * @param {Boolean} active - Whether the new window should be focused when created. - * @param {String} homepageURL - The URL for the new window. + * @param {String} newTabOrWindowURL - The URL for the new window. * */ - gamepad.messageListenerUtils.openNewWindow = function (active, homepageURL) { + gamepad.messageListenerUtils.openNewWindow = function (active, newTabOrWindowURL) { var windowConfig = { - url: homepageURL, + url: newTabOrWindowURL, focused: active }; if (active) { @@ -346,7 +346,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE // On the client side, we check the control state before triggering // these, so the method signature is simply `action(actionOptions)`. openNewTab: async function (actionOptions) { - return await gamepad.messageListenerUtils.openNewTab(actionOptions.active, actionOptions.homepageURL); + return await gamepad.messageListenerUtils.openNewTab(actionOptions.active, actionOptions.newTabOrWindowURL); }, closeCurrentTab: async function (actionOptions) { if (actionOptions.tabId) { @@ -363,7 +363,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE } }, openNewWindow: async function (actionOptions) { - return await gamepad.messageListenerUtils.openNewWindow(actionOptions.active, actionOptions.homepageURL); + return await gamepad.messageListenerUtils.openNewWindow(actionOptions.active, actionOptions.newTabOrWindowURL); }, maximizeWindow: async function (actionOptions) { @@ -406,8 +406,8 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE chrome.runtime.onConnect.addListener(function (port) { port.onMessage.addListener(async function (actionOptions) { // Execute the actions only if the action data is available. - if (actionOptions.actionName) { - var action = messageListener[actionOptions.actionName]; + if (actionOptions.action) { + var action = messageListener[actionOptions.action]; // Trigger the action only if a valid action is found. if (action) { diff --git a/src/js/configuration_panel/button-listeners.js b/src/js/configuration_panel/button-listeners.js deleted file mode 100644 index 1df3c7d..0000000 --- a/src/js/configuration_panel/button-listeners.js +++ /dev/null @@ -1,330 +0,0 @@ -/* -Copyright (c) 2023 The Gamepad Navigator Authors -See the AUTHORS.md file at the top-level directory of this distribution and at -https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. - -Licensed under the BSD 3-Clause License. You may not use this file except in -compliance with this License. - -You may obtain a copy of the BSD 3-Clause License at -https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE -*/ - -/* global gamepad, chrome */ - -(function (fluid) { - "use strict"; - - fluid.registerNamespace("gamepad.configurationPanel.buttonListeners"); - - /** - * TODO: Migrate to Fluid View Components, if needed. Refer: - * https://github.com/fluid-lab/gamepad-navigator/issues/40 - */ - - /** - * - * Set all the input action dropdown menus' value to none. - * - * @param {Object} that - The configurationPanel component. - * - */ - gamepad.configurationPanel.buttonListeners.setAllToNone = function (that) { - // Get the list of all dropdowns in the configuration panel. - var actionDropdownMenus = document.querySelectorAll(".action-dropdown"); - - // Set all dropdown values to none. - fluid.each(actionDropdownMenus, function (actionDropdown) { - if (fluid.isDOMNode(actionDropdown)) { - // Set selected option to none. - actionDropdown.selectedIndex = 0; - - // Hide all other input menu configuration options. - that.changeConfigMenuOptions(actionDropdown); - } - }); - - // Toggle the "Save Changes" button. - that.toggleSaveAndDiscardButtons(); - }; - - /** - * - * Set all the input action dropdown menus' value to their default value. - * - * @param {Object} that - The configurationPanel component. - * - */ - gamepad.configurationPanel.buttonListeners.setToDefault = function (that) { - // Get the list of all dropdowns in the configuration panel. - var configurationMenus = document.querySelectorAll(".menu-item"); - - /** - * Iterate through each input's configuration menu and set configuration option - * values to the default ones. - */ - fluid.each(configurationMenus, function (configurationMenu, index) { - if (fluid.isDOMNode(configurationMenu)) { - var isAxes = index / 16 >= 1, - inputIndex = index % 16, - actionData = that.model.map[isAxes ? "axes" : "buttons"][inputIndex]; - - // Set the default option of the action dropdown. - fluid.find(configurationMenu.querySelector(".action-dropdown").options, function (actionOption, actionIndex) { - if (actionOption.value === actionData.defaultAction) { - configurationMenu.querySelector(".action-dropdown").selectedIndex = actionIndex; - return true; - } - }); - - // Set the default value of the speed factor input. - configurationMenu.querySelector(".speed-factor").value = actionData.speedFactor; - - // Mark the checkbox if it's checked by default. - configurationMenu.querySelector(".checkbox").checked = actionData[isAxes ? "invert" : "background"]; - - // Display/hide other configuration options as per the value of dropdown. - that.changeConfigMenuOptions(configurationMenu.querySelector(".action-dropdown")); - } - }); - - // Toggle the "Save Changes" button. - that.toggleSaveAndDiscardButtons(); - }; - - /** - * - * Discards the unsaved changes and restores the last saved changes. - * - * @param {Object} that - The configurationPanel component. - * - */ - gamepad.configurationPanel.buttonListeners.discardChanges = function (that) { - // Get all the input configuration menus. - var configurationMenus = document.querySelectorAll(".menu-item"); - - // Write the new gamepadConfiguration in Chrome's localStorage. - chrome.storage.local.get(["gamepadConfiguration"], function (configWrapper) { - var isStoredData = configWrapper.gamepadConfiguration ? true : false, - gamepadConfiguration = configWrapper.gamepadConfiguration || that.model.map; - - // Set the values of all configuration options as being used currently. - fluid.each(configurationMenus, function (configurationMenu, menuIndex) { - if (fluid.isDOMNode(configurationMenu)) { - var inputIndex = menuIndex % 16, - isAxes = menuIndex / 16 >= 1; - - // Obtain the configuration for the current input. - var inputConfiguration = gamepadConfiguration[isAxes ? "axes" : "buttons"][inputIndex]; - - // Set the value of the dropdown action for the current input. - var actionValue = inputConfiguration[isStoredData ? "currentAction" : "defaultAction"], - actionDropdown = configurationMenu.querySelector(".action-dropdown"); - fluid.find(actionDropdown.options, function (actionOption, actionIndex) { - if (actionOption.value === actionValue) { - actionDropdown.selectedIndex = actionIndex; - return true; - } - }); - - // Display/hide other configuration options as per the value of dropdown. - that.changeConfigMenuOptions(actionDropdown); - - // Set the value of the speed factor for the current input. - var speedFactorElement = configurationMenu.querySelector(".speed-factor"); - if (!speedFactorElement.hasAttribute("disabled")) { - var speedFactorValue = inputConfiguration.speedFactor; - speedFactorElement.value = speedFactorValue; - } - - // Set the checkbox for the current input. - var checkboxElement = configurationMenu.querySelector(".speed-factor"); - if (!checkboxElement.hasAttribute("disabled")) { - var isChecked = inputConfiguration[isAxes ? "invert" : "background"]; - checkboxElement.checked = isChecked; - } - } - }); - - // Disable the "Discard Changes" and "Save Changes" button. - that.toggleSaveAndDiscardButtons(); - }); - }; - - /** - * - * Store the gamepad configuration from the configuration panel when triggered. - * - * @param {Object} that - The configurationPanel component. - * @param {String} configurationName - The name with which the configuration should be saved. - * - */ - gamepad.configurationPanel.buttonListeners.storeChanges = function (that, configurationName) { - // Get all the input configuration menus. - var configurationMenus = document.querySelectorAll(".menu-item"), - gamepadConfiguration = { - buttons: {}, - axes: {} - }; - - // Save all the configuration options inside the gamepadConfiguration object. - fluid.each(configurationMenus, function (configurationMenu, menuIndex) { - if (fluid.isDOMNode(configurationMenu)) { - var inputIndex = menuIndex % 16, - isAxes = menuIndex / 16 >= 1, - inputConfiguration = {}; - - // Obtain and store the selected action for the current input. - var currentAction = fluid.get(configurationMenu.querySelector(".action-dropdown"), "value"); - inputConfiguration.currentAction = currentAction; - - /** - * Obtain and insert the new speed factor value inside the - * gamepadConfiguration object (if not disabled). - */ - var speedFactorElement = configurationMenu.querySelector(".speed-factor"); - if (!speedFactorElement.hasAttribute("disabled")) { - var speedFactorValue = parseFloat(speedFactorElement.value); - - // Reduce the speed factor value to 2.5 if it's more than that. - inputConfiguration.speedFactor = Math.min(2.5, speedFactorValue); - } - - /** - * Insert the third configuration option checkbox value inside the - * gamepadConfiguration object (if not disabled). - */ - var checkboxValueElement = configurationMenu.querySelector(".checkbox"); - if (!checkboxValueElement.hasAttribute("disabled")) { - var thirdConfigurationOption = isAxes ? "invert" : "background"; - inputConfiguration[thirdConfigurationOption] = checkboxValueElement.checked; - } - - // Save the input's configuration in the gamepadConfiguration object. - gamepadConfiguration[isAxes ? "axes" : "buttons"][inputIndex] = inputConfiguration; - } - }); - - // Remove the old configuration from Chrome's localStorage. - chrome.storage.local.remove([configurationName], function () { - var configurationWrapper = {}; - configurationWrapper[configurationName] = gamepadConfiguration; - - // Save the new configuration. - chrome.storage.local.set(configurationWrapper, function () { - /** - * Toggle (disable) the buttons if the data is stored as - * "gamepadConfiguration" and not the unsaved changes. - */ - if (configurationName === "gamepadConfiguration") { - that.toggleSaveAndDiscardButtons(); - } - }); - }); - }; - - /** - * - * Toggle the "Save Changes" and "Discard Changes" buttons when the input - * configuration is changed. - * - * @param {Object} that - The configurationPanel component. - * @param {Object} saveChangesButton - The "Save Changes" button on the panel. - * @param {Object} discardButton - The "Discard Changes" button on the panel. - * - */ - gamepad.configurationPanel.buttonListeners.toggleSaveAndDiscardButtons = function (that, saveChangesButton, discardButton) { - saveChangesButton = saveChangesButton[0]; - discardButton = discardButton[0]; - chrome.storage.local.get(["gamepadConfiguration"], function (gamepadConfigurationWrapper) { - // Get the list of all dropdowns in the configuration panel. - var configurationMenus = document.querySelectorAll(".menu-item"); - - /** - * Iterate through each input's configuration menu and compare whether any - * configuration option has changed. - */ - var isChanged = fluid.find(configurationMenus, function (configurationMenu, index) { - if (fluid.isDOMNode(configurationMenu)) { - var isAxes = index / 16 >= 1, - inputIndex = index % 16, - isStoredData = gamepadConfigurationWrapper.gamepadConfiguration !== undefined; - - // Get data about the initial values of the configuration options. - var initialConfiguration = gamepadConfigurationWrapper.gamepadConfiguration || that.model.map, - initialInputData = initialConfiguration[isAxes ? "axes" : "buttons"][inputIndex]; - - /** - * Enable the "Save Changes" and "Discard Changes" buttons if - * dropdown option is changed. - */ - var actionDropdown = configurationMenu.querySelector(".action-dropdown"), - currentDropdownValue = actionDropdown.value, - initialDropdownValue = initialInputData[isStoredData ? "currentAction" : "defaultAction"] || "null"; - if (currentDropdownValue !== initialDropdownValue) { - saveChangesButton.removeAttribute("disabled"); - discardButton.removeAttribute("disabled"); - - /** - * Store the unsaved changes to avoid loss of configuration if - * the panel is closed temporarily. - */ - that.storeUnsavedChanges(); - return true; - } - - /** - * Enable the "Save Changes" and "Discard Changes" buttons if the - * value of speedFactor is changed. - */ - var speedFactorElement = configurationMenu.querySelector(".speed-factor"); - if (!speedFactorElement.hasAttribute("disabled")) { - var initialSpeedFactorValue = initialInputData.speedFactor, - currentSpeedFactorValue = parseFloat(speedFactorElement.value); - if (initialSpeedFactorValue !== currentSpeedFactorValue) { - saveChangesButton.removeAttribute("disabled"); - discardButton.removeAttribute("disabled"); - - /** - * Store the unsaved changes to avoid loss of configuration if - * the panel is closed temporarily. - */ - that.storeUnsavedChanges(); - return true; - } - } - - /** - * Enable the "Save Changes" and "Discard Changes" buttons if the - * value of third configuraton option is changed. - */ - var checkboxElement = configurationMenu.querySelector(".checkbox"); - if (!checkboxElement.hasAttribute("disabled")) { - var wasCheckboxChecked = initialInputData[isAxes ? "invert" : "background"], - isCheckboxChecked = checkboxElement.checked; - if (wasCheckboxChecked !== isCheckboxChecked) { - saveChangesButton.removeAttribute("disabled"); - discardButton.removeAttribute("disabled"); - - /** - * Store the unsaved changes to avoid loss of configuration if - * the panel is closed temporarily. - */ - that.storeUnsavedChanges(); - return true; - } - } - } - }); - - if (!isChanged) { - // Remove the unsaved changes stored in the Chrome's storage. - chrome.storage.local.remove(["unsavedConfiguration"], function () { - // Disable the "Save Changes" and "Discard Changes" buttons. - saveChangesButton.setAttribute("disabled", ""); - discardButton.setAttribute("disabled", ""); - }); - } - }); - }; -})(fluid); diff --git a/src/js/configuration_panel/configuration-panel.js b/src/js/configuration_panel/configuration-panel.js deleted file mode 100644 index 675b230..0000000 --- a/src/js/configuration_panel/configuration-panel.js +++ /dev/null @@ -1,357 +0,0 @@ -/* -Copyright (c) 2023 The Gamepad Navigator Authors -See the AUTHORS.md file at the top-level directory of this distribution and at -https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. - -Licensed under the BSD 3-Clause License. You may not use this file except in -compliance with this License. - -You may obtain a copy of the BSD 3-Clause License at -https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE -*/ - -/* global gamepad, chrome */ - -(function (fluid) { - "use strict"; - - /** - * TODO: Associate component model data with configuration input elements. Refer: - * https://github.com/fluid-lab/gamepad-navigator/issues/41 - */ - - fluid.registerNamespace("gamepad.configMaps"); - fluid.registerNamespace("gamepad.configurationPanel"); - fluid.registerNamespace("gamepad.configurationPanel.createPanelUtils"); - fluid.registerNamespace("gamepad.configurationPanel.handleEvents"); - fluid.registerNamespace("gamepad.configurationPanel.buttonListeners"); - - fluid.defaults("gamepad.configurationPanel", { - gradeNames: ["gamepad.configMaps", "fluid.viewComponent"], - selectors: { - inputList: ".input-list", - configurationMenu: ".configuration-menu", - buttonsContainer: ".buttons-container", - setAllToNoneButton: "#set-to-none", - restoreDefaultsButton: "#set-to-default", - saveChangesButton: "#save-changes", - discardButton: "#discard-changes" - }, - listeners: { - "onCreate.loadConfigurationPanel": "{that}.createMenu", - "onCreate.handleSwitching": { - funcName: "{that}.handleSwitching", - after: "loadConfigurationPanel" - } - }, - description: { - buttons: { - "0": "Button 0: A (Xbox), ⨯ (PS4)", - "1": "Button 1: B (Xbox), O (PS4)", - "2": "Button 2: X (Xbox), ◻ (PS4)", - "3": "Button 3: Y (Xbox), △ (PS4)", - "4": "Button 4: Left Bumper", - "5": "Button 5: Right Bumper", - "6": "Button 6: Left Trigger", - "7": "Button 7: Right Trigger", - "8": "Button 8: Back (Xbox), Share (PS4)", - "9": "Button 9: Start (Xbox), Options (PS4)", - "10": "Button 10: Left Thumbstick Button", - "11": "Button 11: Right Thumbstick Button", - "12": "Button 12: D-Pad Up Button", - "13": "Button 13: D-Pad Down Button", - "14": "Button 14: D-Pad Left Button", - "15": "Button 15: D-Pad Right Button" - }, - axes: { - "0": "Left Thumbstick Horizontal Direction", - "1": "Left Thumbstick Vertical Direction", - "2": "Right Thumbstick Horizontal Direction", - "3": "Right Thumbstick Vertical Direction" - } - }, - message: { - buttons: { - null: "None", - click: "Click", - previousPageInHistory: "History back button", - nextPageInHistory: "History next button", - reverseTab: "Focus on the previous element", - forwardTab: "Focus on the next element", - scrollLeft: "Scroll left", - scrollRight: "Scroll right", - scrollUp: "Scroll up", - scrollDown: "Scroll down", - goToPreviousTab: "Switch to the previous browser tab", - goToNextTab: "Switch to the next browser tab", - closeCurrentTab: "Close current browser tab", - openNewTab: "Open a new tab", - closeCurrentWindow: "Close current browser window", - openNewWindow: "Open a new browser window", - goToPreviousWindow: "Switch to the previous browser window", - goToNextWindow: "Switch to the next browser window", - zoomIn: "Zoom-in on the active web page", - zoomOut: "Zoom-out on the active web page", - maximizeWindow: "Maximize the current browser window", - restoreWindowSize: "Restore the size of current browser window", - reopenTabOrWindow: "Re-open the last closed tab or window", - sendArrowLeft: "Send left arrow to the focused element.", - sendArrowRight: "Send right arrow to the focused element.", - sendArrowUp: "Send up arrow to the focused element.", - sendArrowDown: "Send down arrow to the focused element.", - openActionLauncher: "Open Action Launcher", - openSearchKeyboard: "Open Search" - }, - axes: { - null: "None", - scrollHorizontally: "Scroll horizontally", - scrollVertically: "Scroll vertically", - thumbstickHistoryNavigation: "History navigation", - thumbstickTabbing: "Focus on the previous/next element", - thumbstickZoom: "Zoom in or out on the active web page", - thumbstickWindowSize: "Maximize/restore the size of current browser window", - thumbstickHorizontalArrows: "Send left/right arrows to the current focused element.", - thumbstickVerticalArrows: "Send up/down arrows to the current focused element." - } - }, - // Describes the actions that use a particular configuration option. - actions: { - speedFactorOption: [ - "reverseTab", - "forwardTab", - "scrollLeft", - "scrollRight", - "scrollUp", - "scrollDown", - "scrollHorizontally", - "scrollVertically", - "thumbstickTabbing", - "thumbstickHorizontalArrows", - "thumbstickVerticalArrows" - ], - backgroundOption: ["openNewTab", "openNewWindow"], - invertOption: [ - "scrollHorizontally", - "scrollVertically", - "thumbstickHistoryNavigation", - "thumbstickTabbing", - "thumbstickZoom", - "thumbstickWindowSize", - "thumbstickHorizontalArrows", - "thumbstickVerticalArrows" - ] - }, - invokers: { - createMenu: { - funcName: "gamepad.configurationPanel.createMenu", - args: [ - "{that}", - "{that}.dom.inputList", - "{that}.dom.configurationMenu", - "{that}.dom.saveChangesButton", - "{that}.dom.discardButton" - ] - }, - createInputActionDropdown: { - funcName: "gamepad.configurationPanel.createPanelUtils.createInputActionDropdown", - args: [ - "{that}", - "{arguments}.0", - "{arguments}.1", - "{arguments}.2", - "{arguments}.3" - ] - }, - createSpeedFactorOption: { - funcName: "gamepad.configurationPanel.createPanelUtils.createSpeedFactorOption", - args: ["{arguments}.0", "{arguments}.1", "{arguments}.2"] - }, - createCheckbox: { - funcName: "gamepad.configurationPanel.createPanelUtils.createCheckbox", - args: [ - "{arguments}.0", - "{arguments}.1", - "{arguments}.2", - "{arguments}.3" - ] - }, - attachListeners: { - funcName: "gamepad.configurationPanel.attachListeners", - args: [ - "{that}", - "{that}.dom.setAllToNoneButton", - "{that}.dom.restoreDefaultsButton", - "{that}.dom.saveChangesButton", - "{that}.dom.discardButton" - ] - }, - handleSwitching: { - funcName: "gamepad.configurationPanel.handleEvents.switchMenu", - args: ["{that}.dom.inputList", "{that}.dom.configurationMenu"] - }, - modifyActionDropdownMenu: { - funcName: "gamepad.configurationPanel.handleEvents.modifyActionDropdownMenu", - args: ["{that}"] - }, - listenActionDropdownChanges: { - funcName: "gamepad.configurationPanel.handleEvents.listenActionDropdownChanges", - args: ["{that}"] - }, - changeConfigMenuOptions: { - funcName: "gamepad.configurationPanel.handleEvents.changeConfigMenuOptions", - args: ["{that}", "{arguments}.0"] - }, - setAllToNoneListener: { - funcName: "gamepad.configurationPanel.buttonListeners.setAllToNone", - args: ["{that}"] - }, - setToDefaultListener: { - funcName: "gamepad.configurationPanel.buttonListeners.setToDefault", - args: ["{that}"] - }, - discardChangesListener: { - funcName: "gamepad.configurationPanel.buttonListeners.discardChanges", - args: ["{that}"] - }, - saveChangesListener: { - funcName: "gamepad.configurationPanel.buttonListeners.storeChanges", - args: ["{that}", "gamepadConfiguration"] - }, - toggleSaveAndDiscardButtons: { - funcName: "gamepad.configurationPanel.buttonListeners.toggleSaveAndDiscardButtons", - args: ["{that}", "{that}.dom.saveChangesButton", "{that}.dom.discardButton"] - }, - storeUnsavedChanges: { - funcName: "gamepad.configurationPanel.buttonListeners.storeChanges", - args: ["{that}", "unsavedConfiguration"] - } - } - }); - - /** - * - * Create a configuration menu on the Chrome extension's popup window. - * - * @param {Object} that - The configurationPanel component. - * @param {Array} inputList - The jQuery selector of the input list. - * @param {Array} configurationMenu - The jQuery selector of the configuration menu. - * @param {Object} saveChangesButton - The "Save Changes" button on the panel. - * @param {Object} discardButton - The "Discard Changes" button on the panel. - * - */ - gamepad.configurationPanel.createMenu = function (that, inputList, configurationMenu, saveChangesButton, discardButton) { - // Clear all the content inside the configuration menu. - configurationMenu = configurationMenu[0]; - configurationMenu.innerHTML = ""; - - /** - * Create the configuration menu for each input and inject it inside the - * configuration panel. - */ - chrome.storage.local.get(["gamepadConfiguration", "unsavedConfiguration"], function (configWrapper) { - var totalGamepadInputs = 20, - isUnsaved = configWrapper.unsavedConfiguration ? true : false, - storedConfig = fluid.get(configWrapper, isUnsaved ? "unsavedConfiguration" : "gamepadConfiguration"); - - for (var inputCounter = 0; inputCounter < totalGamepadInputs; inputCounter++) { - // Compute input label and index of input. - var inputIndex = inputCounter % 16, - isAxes = inputCounter / 16 >= 1, - inputType = isAxes ? "axes" : "buttons"; - - // Set attributes and text of the input name dropdown. - var inputOption = document.createElement("option"); - inputOption.innerHTML = that.options.description[inputType][inputIndex]; - inputOption.value = inputType + "-" + inputIndex; - inputList[0].appendChild(inputOption); - - // Create a container for the particular input's configuration options. - var inputMenuItem = document.createElement("div"); - - // Set properties/attributes of the container element. - var inputIdentifier = inputType + "-" + inputIndex; - inputMenuItem.classList.add("menu-item", inputIdentifier); - inputMenuItem.style.display = "none"; - - var isStored = storedConfig ? true : false, - gamepadConfig = isStored ? storedConfig : that.model.map; - - /** - * Obtain and use the default input data for setting initial values on - * the panel, if the stored gamepad configuration data is unavailable. - */ - var inputConfig = gamepadConfig[inputType][inputIndex], - actionValue = inputConfig[isStored ? "currentAction" : "defaultAction"], - speedFactor = fluid.get(inputConfig, "speedFactor"), - checkboxValue = fluid.get(inputConfig, isAxes ? "invert" : "background"); - - // Create the configuration option inputs for the current input. - that.createInputActionDropdown( - inputIdentifier, - inputMenuItem, - inputType, - actionValue - ); - that.createSpeedFactorOption(inputIdentifier, inputMenuItem, speedFactor); - that.createCheckbox( - inputIdentifier, - inputMenuItem, - isAxes, - checkboxValue - ); - - // Inject the input menu inside the configuration menu/panel. - configurationMenu.appendChild(inputMenuItem); - - /** - * Enable the "Discard Changes" button and "Save Changes" button if the - * configuration is unsaved. - */ - if (isUnsaved) { - discardButton[0].removeAttribute("disabled"); - saveChangesButton[0].removeAttribute("disabled"); - } - } - - /** - * Modify the configuration panel according to the values of the - * configuration options and attach listeners to the configuration - * options and buttons. - */ - that.modifyActionDropdownMenu(); - that.listenActionDropdownChanges(); - that.attachListeners(); - }); - }; - - /** - * - * Attach listeners to the configuration options and buttons. - * - * @param {Object} that - The configurationPanel component. - * @param {Object} setAllToNoneButton - The "Set All to None" button on the panel. - * @param {Object} restoreDefaultsButton - The "Restore Default Controls" button on the panel. - * @param {Object} saveChangesButton - The "Save Changes" button on the panel. - * @param {Object} discardButton - The "Discard Changes" button on the panel. - * - */ - gamepad.configurationPanel.attachListeners = function (that, setAllToNoneButton, restoreDefaultsButton, saveChangesButton, discardButton) { - // Attach listener to all configuration options to toggle "Save Changes" button. - var configurationOptions = document.querySelectorAll(".action-dropdown, .speed-factor, .checkbox"); - fluid.each(configurationOptions, function (configurationOption) { - if (fluid.isDOMNode(configurationOption)) { - configurationOption.addEventListener("input", that.toggleSaveAndDiscardButtons); - } - }); - - // Attach click listener to all the buttons. - setAllToNoneButton.click(that.setAllToNoneListener); - restoreDefaultsButton.click(that.setToDefaultListener); - saveChangesButton.click(that.saveChangesListener); - discardButton.click(that.discardChangesListener); - }; - - window.onload = function () { - gamepad.configurationPanel(".configuration-dashboard"); - }; -})(fluid); diff --git a/src/js/configuration_panel/create-panel-utils.js b/src/js/configuration_panel/create-panel-utils.js deleted file mode 100644 index 181c264..0000000 --- a/src/js/configuration_panel/create-panel-utils.js +++ /dev/null @@ -1,156 +0,0 @@ -/* -Copyright (c) 2023 The Gamepad Navigator Authors -See the AUTHORS.md file at the top-level directory of this distribution and at -https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. - -Licensed under the BSD 3-Clause License. You may not use this file except in -compliance with this License. - -You may obtain a copy of the BSD 3-Clause License at -https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE -*/ - -/* global gamepad */ - -(function (fluid) { - "use strict"; - - fluid.registerNamespace("gamepad.configurationPanel.createPanelUtils"); - - // TODO: Fix the "inputType" parameter type in the JSDoc comments. - - /** - * - * Create the input action label and its dropdown and inject both into the - * configuration menu (of the given input). - * - * @param {Object} that - The configurationPanel component. - * @param {String} inputIdentifier - The String containing input type with its index. - * For example, "button-0". - * @param {Object} configMenu - The configuration menu (container) for a given input. - * @param {Boolean} inputType - The type of the gamepad input, i.e., "axes" or "buttons". - * @param {Object} currentValue - The current value of the input action dropdown for - * a given input. - * - */ - gamepad.configurationPanel.createPanelUtils.createInputActionDropdown = function (that, inputIdentifier, configMenu, inputType, currentValue) { - // Create a label for the input action dropdown and set its inner text. - var actionLabel = document.createElement("label"); - actionLabel.innerHTML = "Action:"; - - // Set the attributes and class names of the input action label. - actionLabel.setAttribute("for", inputIdentifier + "-action"); - actionLabel.classList.add("action-label", inputIdentifier + "-child"); - - // Inject the input action label into configuration menu (of the given input). - configMenu.appendChild(actionLabel); - - // Create the input action dropdown (select) for the given input. - var inputSelectMenu = document.createElement("select"); - - // Set the value of the dropdown to the action currently selected. - var actionsList = that.options.message[inputType]; - fluid.each(actionsList, function (actionLabel, actionValue) { - // Create an option for the action and set its value and label. - var option = document.createElement("option"); - option.setAttribute("value", actionValue); - option.innerHTML = actionLabel; - - // Mark the option as selected if it is being used. - if (currentValue === actionValue) { - option.setAttribute("selected", ""); - } - - // Inject the option into the dropdown. - inputSelectMenu.appendChild(option); - }); - - // Set other attributes and class names of the input action dropdown menu. - inputSelectMenu.setAttribute("name", inputIdentifier + "-action"); - inputSelectMenu.classList.add("action-dropdown", inputIdentifier + "-child"); - - // Inject the dropdown menu into configuration menu (of the given input). - configMenu.appendChild(inputSelectMenu); - }; - - /** - * - * Create the speed factor label and its input box and inject both into the - * configuration menu (of the given input). - * - * @param {String} inputIdentifier - The String containing input type with its index. - * For example, "button-0". - * @param {Object} configMenu - The configuration menu (container) for a given input. - * @param {Object} currentValue - The current value of speedFactor for a given input - * (if applicable). - * - */ - gamepad.configurationPanel.createPanelUtils.createSpeedFactorOption = function (inputIdentifier, configMenu, currentValue) { - // Create a speed factor label and set its inner text. - var speedFactorLabel = document.createElement("label"); - speedFactorLabel.innerHTML = "Speed Factor:"; - - // Set the attributes and class names of the speed factor label. - speedFactorLabel.setAttribute("for", inputIdentifier + "-speedFactor"); - speedFactorLabel.classList.add("speed-factor-label", inputIdentifier + "-child"); - - // Inject the speed factor label into configuration menu (of the given input). - configMenu.appendChild(speedFactorLabel); - - // Create a speed factor numeric input box and set its default value. - var speedFactorInput = document.createElement("input"); - speedFactorInput.setAttribute("type", "number"); - speedFactorInput.value = currentValue; - - // Set the minimun, maximum, and increment value of the input box. - speedFactorInput.setAttribute("step", 0.1); - speedFactorInput.setAttribute("min", 0.5); - speedFactorInput.setAttribute("max", 2.5); - - // Set other attributes and class names of the speed factor input box. - speedFactorInput.setAttribute("name", inputIdentifier + "-speedFactor"); - speedFactorInput.classList.add("speed-factor", inputIdentifier + "-child"); - - // Inject speed factor input box into configuration menu (of the given input). - configMenu.appendChild(speedFactorInput); - }; - - /** - * - * Create the third configuration option label and checkbox and inject both into the - * configuration menu (of the given input). - * - * @param {String} inputIdentifier - The String containing input type with its index. - * For example, "button-0". - * @param {Object} configMenu - The configuration menu (container) for a given input. - * @param {Boolean} isAxes - Whether the current input is "axes". - * @param {Object} currentValue - The current value of the third configuration option - * for a given input (if applicable). - * - */ - gamepad.configurationPanel.createPanelUtils.createCheckbox = function (inputIdentifier, configMenu, isAxes, currentValue) { - // Create the third configuration option label and set its inner text. - var thirdConfigurationOptionLabel = document.createElement("label"); - thirdConfigurationOptionLabel.innerHTML = isAxes ? "Invert Action" : "Open new tab/window in background"; - - // Set the attributes and class names of the third configuration option label. - var forSuffix = isAxes ? "invert" : "background"; - thirdConfigurationOptionLabel.setAttribute("for", inputIdentifier + "-" + forSuffix); - thirdConfigurationOptionLabel.classList.add("checkbox-label", inputIdentifier + "-child"); - - // Inject the checkbox label into configuration menu (of the given input). - configMenu.appendChild(thirdConfigurationOptionLabel); - - // Create the third configuration option checkbox and set its value. - var thirdConfigurationOption = document.createElement("input"); - thirdConfigurationOption.setAttribute("type", "checkbox"); - thirdConfigurationOption.checked = currentValue; - - // Set other attributes and class names of the checkbox. - thirdConfigurationOption.setAttribute("name", inputIdentifier + "-" + forSuffix); - thirdConfigurationOption.classList.add("checkbox", inputIdentifier + "-child"); - - // Inject the checkbox into configuration menu (of the given input). - configMenu.appendChild(thirdConfigurationOption); - }; -})(fluid); diff --git a/src/js/configuration_panel/panel-events.js b/src/js/configuration_panel/panel-events.js deleted file mode 100644 index 121b97a..0000000 --- a/src/js/configuration_panel/panel-events.js +++ /dev/null @@ -1,173 +0,0 @@ -/* -Copyright (c) 2023 The Gamepad Navigator Authors -See the AUTHORS.md file at the top-level directory of this distribution and at -https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. - -Licensed under the BSD 3-Clause License. You may not use this file except in -compliance with this License. - -You may obtain a copy of the BSD 3-Clause License at -https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE -*/ - -/* global gamepad */ - -(function (fluid) { - "use strict"; - - fluid.registerNamespace("gamepad.configurationPanel.handleEvents"); - - /** - * - * Handles switching between the various inputs' configuration menu. - * - * @param {Object} inputList - The jQuery selector of the input name menu. - * @param {Array} configurationMenu - The jQuery selector of the configuration menu. - * - */ - gamepad.configurationPanel.handleEvents.switchMenu = function (inputList, configurationMenu) { - inputList = inputList[0]; - configurationMenu = configurationMenu[0]; - - // Attach listener to the input dropdown. - inputList.addEventListener("change", function (event) { - var inputMenuClassName = event.target.value, - inputMenus = configurationMenu.querySelectorAll(".menu-item"); - - // Hide the currently visible configuration menus. - fluid.find(inputMenus, function (inputMenu) { - if (fluid.isDOMNode(inputMenu) && inputMenu.style.display !== "none") { - inputMenu.style.display = "none"; - return true; - } - }); - - // Display the input configuration menu according to the selected option. - var currentInputMenu = configurationMenu.getElementsByClassName(inputMenuClassName)[0]; - currentInputMenu.style.display = "grid"; - }); - }; - - /** - * - * Displays only the relevant configuration options for each action after the - * configuration panel is created. - * - * @param {Object} that - The configurationPanel component. - * - */ - gamepad.configurationPanel.handleEvents.modifyActionDropdownMenu = function (that) { - // Get the list of all configuration menus on the configuration panel. - var inputMenusArray = document.querySelectorAll(".menu-item"); - - /** - * Set dropdown values to their default values and other configuration options - * accordingly. - */ - fluid.each(inputMenusArray, function (inputMenu) { - if (fluid.isDOMNode(inputMenu)) { - that.changeConfigMenuOptions(inputMenu.querySelector(".action-dropdown")); - } - }); - }; - - /** - * - * Attaches listener to the action dropdowns to display only the relevant - * configuration options according to the new action chosen by the user. - * - * @param {Object} that - The configurationPanel component. - * - */ - gamepad.configurationPanel.handleEvents.listenActionDropdownChanges = function (that) { - // Get the list of all dropdowns in the configuration panel. - var actionDropdowns = document.querySelectorAll(".action-dropdown"); - - // Attach change listener to all dropdown menus. - fluid.each(actionDropdowns, function (actionDropdown) { - if (fluid.isDOMNode(actionDropdown)) { - actionDropdown.addEventListener("change", function (event) { - that.changeConfigMenuOptions(event.target); - }); - } - }); - }; - - /** - * TODO: Make another sub-component managing the mapping for one control, its - * visibility, and settings and relay them into the parent component. - * Refer: - * https://github.com/fluid-lab/gamepad-navigator/issues/40 - */ - - /** - * - * Displays only the relevant configuration options for the given dropdown according - * to the chosen action (value of the dropdown). - * - * @param {Object} that - The configurationPanel component. - * @param {Object} dropdownMenu - The input action dropdown menu of an input menu. - * - */ - gamepad.configurationPanel.handleEvents.changeConfigMenuOptions = function (that, dropdownMenu) { - /** - * TODO: Use viewComponent infrastructure instead of the class selectors. - * Refer: - * https://github.com/fluid-lab/gamepad-navigator/issues/40 - */ - var selectedAction = $(dropdownMenu).val(), - dropdownClassName = dropdownMenu.classList[1], - currentInputMenuItems = document.getElementsByClassName(dropdownClassName); - - /** - * Show speed factor input box and its label if selected option is applicable for - * using speed factor. Othewise, hide the speed factor input box and label and - * increase the input width. - */ - if (that.options.actions.speedFactorOption.includes(selectedAction)) { - currentInputMenuItems[2].classList.remove("hidden"); - currentInputMenuItems[3].classList.remove("hidden"); - - // Disable the speed factor input box. - currentInputMenuItems[3].removeAttribute("disabled"); - - // Reduce the width of the action dropdown. - dropdownMenu.classList.add("reduced"); - } - else { - currentInputMenuItems[2].classList.add("hidden"); - currentInputMenuItems[3].classList.add("hidden"); - - // Reset the speed factor input box value. - currentInputMenuItems[3].value = "1"; - - // Remove the disabled attribute from the input box. - currentInputMenuItems[3].setAttribute("disabled", ""); - - // Increase the width of the action dropdown. - dropdownMenu.classList.remove("reduced"); - } - - /** - * Show checkbox if selected option is applicable for opening tabs/windows in - * background or is invertible. Otherwise, hide the checkboxes and their labels. - */ - if (that.options.actions.backgroundOption.includes(selectedAction) || that.options.actions.invertOption.includes(selectedAction)) { - currentInputMenuItems[4].classList.remove("hidden"); - currentInputMenuItems[5].classList.remove("hidden"); - - // Disable the checkbox. - currentInputMenuItems[5].removeAttribute("disabled"); - } - else { - currentInputMenuItems[4].classList.add("hidden"); - currentInputMenuItems[5].classList.add("hidden"); - - // Reset the checkbox, i.e., uncheck it. - currentInputMenuItems[5].checked = false; - - // Remove the disabled attribute from the checkbox. - currentInputMenuItems[5].setAttribute("disabled", ""); - } - }; -})(fluid); diff --git a/src/js/content_scripts/action-launcher.js b/src/js/content_scripts/action-launcher.js index ae02879..da46e04 100644 --- a/src/js/content_scripts/action-launcher.js +++ b/src/js/content_scripts/action-launcher.js @@ -29,6 +29,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE } }); + // Simulate gamepad input as though all keys and axes were held. fluid.defaults("gamepad.actionLauncher.action", { gradeNames: ["gamepad.templateRenderer"], @@ -38,16 +39,16 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE model: { row: -1, - value: 1, - oldValue: 0, - commonConfiguration: { - homepageURL: "https://www.google.com/" - }, + + axes: {}, + // Our all-powerful button that is always depressed. + buttons: { 0: 1 }, + actionOptions: { - speedFactor: 1, + scrollFactor: 1, invert: false, background: false, - frequency: 100 + repeatRate: 0 } }, @@ -114,27 +115,23 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE var actionFn = fluid.get(inputMapperComponent, actionComponent.model.actionKey); if (actionFn) { - // Simulate a button press and release so that all actions are - // triggered appropriately. + // Simulate a button press so that actions that care about the input value (like scroll) will work. + // We use one that is not ordinarily possible to trigger to avoid any possible conflict with bindings. + inputMapperComponent.applier.change(["buttons", "999"], 1); - // All actions are called with: - // value, oldValue, actionOptions - - // Simulate button down + // Call the action with our simulated button. All actions are called with: actionOptions, inputType, index actionFn( - 1, - 0, - actionComponent.model.actionOptions + actionComponent.model.actionOptions, + "buttons", + "999" ); - // Simulate button up after a delay (100ms by default) + // Remove our fake button press after a delay (100ms by default) setTimeout(function () { - actionFn( - 0, - 1, - actionComponent.model.actionOptions - ); - }, actionComponent.model.frequency); + var transaction = inputMapperComponent.applier.initiate(); + transaction.fireChangeRequest({ path: ["buttons", "999"], type: "DELETE"}); + transaction.commit(); + }, actionComponent.model.repeatRate); } }; @@ -154,111 +151,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE model: { focusedRow: 0, // TODO: Add support for controlling `backgroundOption` in the `openNewTab` and `openNewWindow` actions. - actionDefs: [ - // All button-driven actions, except for the action launcher itself, ordered by subjective "usefulness". - { - key: "openConfigPanel", - description: "Configure Gamepad Navigator" - }, - { - key: "openSearchKeyboard", - description: "Search" - }, - { - key: "openNewWindow", - description: "Open a new browser window" - }, - { - key: "openNewTab", - description: "Open a new tab" - }, - { - key: "goToPreviousWindow", - description: "Switch to the previous browser window" - }, - { - key: "goToNextWindow", - description: "Switch to the next browser window" - }, - { - key: "goToPreviousTab", - description: "Switch to the previous browser tab" - }, - { - key: "goToNextTab", - description: "Switch to the next browser tab" - }, - { - key: "closeCurrentTab", - description: "Close current browser tab" - }, - { - key: "closeCurrentWindow", - description: "Close current browser window" - }, - { - key: "reopenTabOrWindow", - description: "Re-open the last closed tab or window" - }, - { - key: "previousPageInHistory", - description: "History back button" - }, - { - key: "nextPageInHistory", - description: "History next button" - }, - { - key: "maximizeWindow", - description: "Maximize the current browser window" - }, - { - key: "restoreWindowSize", - description: "Restore the size of current browser window" - }, - // These should nearly always already be bound. - { - key: "click", - description: "Click" - }, - { - key: "reverseTab", - description: "Focus on the previous element" - }, - { - key: "forwardTab", - description: "Focus on the next element" - }, - // Here for completeness, but IMO less likely to be used. - { - key: "scrollLeft", - description: "Scroll left", - frequency: 250 - }, - { - key: "scrollRight", - description: "Scroll right", - frequency: 250 - }, - { - key: "scrollUp", - description: "Scroll up", - frequency: 250 - }, - { - key: "scrollDown", - description: "Scroll down", - frequency: 250 - }, - { - key: "zoomIn", - description: "Zoom-in on the active web page" - }, - { - key: "zoomOut", - description: "Zoom-out on the active web page" - } - ] + actionDefs: gamepad.actions.launchable }, dynamicComponents: { @@ -273,10 +166,10 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE actionKey: "{source}.key", description: "{source}.description", actionOptions: { - speedFactor: "{source}.speedFactor", + scrollFactor: "{source}.scrollFactor", invert: "{source}.invert", background: "{source}.background", - frequency: "{source}.frequency" + repeatRate: "{source}.repeatRate" } } } diff --git a/src/js/content_scripts/actions.js b/src/js/content_scripts/actions.js new file mode 100644 index 0000000..e0c0c6a --- /dev/null +++ b/src/js/content_scripts/actions.js @@ -0,0 +1,227 @@ +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ +(function (fluid) { + "use strict"; + var gamepad = fluid.registerNamespace("gamepad"); + + fluid.registerNamespace("gamepad.actions"); + + gamepad.actions.axis = { + thumbstickHistoryNavigation: { + description: "Navigate through history with a stick input", + repeatRate: 0, + invert:false + }, + thumbstickHorizontalArrows: { + description: "Send left or right arrows with a stick input", + repeatRate: 0, + invert:false + }, + thumbstickTabbing: { + description: "Move through the focusable elements using a stick input", + repeatRate: 0, + invert:false + }, + scrollHorizontally: { + description: "Scroll horizontally with a stick input", + repeatRate: 0.1, + scrollFactor: 10, + invert:false + }, + scrollVertically: { + description: "Scroll vertically with a stick input", + repeatRate: 0.1, + scrollFactor: 10, + invert:false + }, + thumbstickVerticalArrows: { + description: "Send up or down arrows with a stick input", + repeatRate: 0.5, + invert:false + }, + thumbstickWindowSize: { + description: "Change the window size with a stick input", + repeatRate: 0, + invert:false + }, + thumbstickZoom: { + description: "Zoom in or out of the window with a stick input", + repeatRate: 1, + invert:false + } + }; + + gamepad.actions.button = { + click: { + description: "Click the focused element" + }, + closeCurrentTab: { + description: "Close the current tab" + }, + closeCurrentWindow: { + description: "Close the current window" + }, + goToNextTab: { + description: "Switch to the next tab", + repeatRate: 1 + }, + goToNextWindow: { + description: "Switch to the next window", + repeatRate: 1 + }, + goToPreviousTab: { + description: "Switch to the previous tab", + repeatRate: 1 + }, + goToPreviousWindow: { + description: "Switch to the previous window", + repeatRate: 1 + }, + maximizeWindow: { + description: "Maximise the window" + }, + nextPageInHistory: { + description: "Switch to the next page in history" + }, + openNewTab: { + description: "Open a new tab", + background: false + }, + openNewWindow: { + description: "Open a new window", + background: false + }, + openActionLauncher: { + description: "Open the action launcher" + }, + openSearchKeyboard: { + description: "Start a search" + }, + openConfigPanel: { + description: "Open the settings panel" + }, + previousPageInHistory: { + description: "Switch to the previous page in history" + }, + reopenTabOrWindow: { + description: "Reopen the most recently closed tab or window" + }, + restoreWindowSize: { + description: "Restore the window to its previous size." + }, + scrollDown: { + description: "Scroll down", + repeatRate: 0.1, + scrollFactor: 10 + }, + scrollLeft: { + description: "Scroll left", + repeatRate: 0.1, + scrollFactor: 10 + }, + scrollRight: { + description: "Scroll right", + repeatRate: 0.1, + scrollFactor: 10 + }, + scrollUp: { + description: "Scroll up", + repeatRate: 0.1, + scrollFactor: 10 + }, + sendKey: { + description: "Send a key to the focused element", + repeatRate: 0 + }, + tabBackward: { + description: "Focus on the previous focusable element", + repeatRate: 0.4 + }, + tabForward: { + description: "Focus on the next focusable element", + repeatRate: 0.4 + }, + zoomIn: { + description: "Zoom in to the current window", + repeatRate: 0.5 + }, + zoomOut: { + description: "Zoom out of the current window", + repeatRate: 0.5 + } + }; + + gamepad.actions.all = fluid.merge({}, gamepad.actions.button, gamepad.actions.axis); + + fluid.registerNamespace("gamepad.actions.keys"); + gamepad.actions.keys.launchable = [ + "openConfigPanel", + "openSearchKeyboard", + "openNewWindow", + "openNewTab", + "goToPreviousWindow", + "goToNextWindow", + "goToPreviousTab", + "goToNextTab", + "closeCurrentTab", + "closeCurrentWindow", + "reopenTabOrWindow", + "previousPageInHistory", + "nextPageInHistory", + "maximizeWindow", + "restoreWindowSize", + "click", + "tabBackward", + "tabForward", + "scrollLeft", + "scrollRight", + "scrollUp", + "scrollDown", + "zoomIn", + "zoomOut" + ]; + + // Derive the list of launchable actions from the above. + gamepad.actions.launchable = (function () { + var originalLaunchableDefs = fluid.filterKeys(gamepad.actions.button, gamepad.actions.keys.launchable); + var reworkedDefs = []; + fluid.each(originalLaunchableDefs, function (launchDef, key) { + var reworkedDef = fluid.copy(launchDef); + reworkedDef.key = key; + + // launcher actions should never repeat. + if (reworkedDef.repeatRate) { + reworkedDef.repeatRate = 0; + } + + reworkedDefs.push(reworkedDef); + }); + return reworkedDefs; + })(); + + gamepad.actions.mapToChoices = function (originalMap) { + var choices = {}; + fluid.each(originalMap, function (value, key) { + choices[key] = fluid.get(value, "description") || value; + }); + return choices; + }; + + fluid.registerNamespace("gamepad.actions.choices"); + + // Derive the actions available for buttons so that we can populate a drop-down. + gamepad.actions.choices.button = gamepad.actions.mapToChoices(gamepad.actions.button); + + // Derive the actions available for axes so that we can populate a drop-down. + gamepad.actions.choices.axis = gamepad.actions.mapToChoices(gamepad.actions.axis); + +})(fluid); diff --git a/src/js/content_scripts/bindings.js b/src/js/content_scripts/bindings.js index 25827f8..5aed9e9 100644 --- a/src/js/content_scripts/bindings.js +++ b/src/js/content_scripts/bindings.js @@ -33,8 +33,8 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE @typedef {Object} ActionOptions @property {String} action - The name of the action. @property {Boolean} [invert] - Whether to invert the direction of motion (for actions that have a direction, like scrolling). - @property {Number} [repeat] - For actions that support continuous operation, how many seconds to wait before repeating the action if the same control is still depressed. - @property {Number} [speed] - How far to navigate in a single action. + @property {Number} [repeatRate] - For actions that support continuous operation, how many seconds to wait before repeating the action if the same control is still depressed. + @property {Number} [scrollFactor] - How far to scroll in a single action. @property {Boolean} [background] - For new windows/tabs, whether to open in the background. @property {String} [key] - For `sendKey`, the key to send. @@ -48,7 +48,6 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE }, // Circle on PS controller, B on Xbox. - // TODO: Make this work only in modals. 1: { action: "sendKey", key: "Escape" @@ -56,15 +55,13 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE // Left Bumper. "4": { - action: "reverseTab", - repeat: 1, - speed: 2.5 + action: "tabBackward", + repeatRate: 0.5 }, // Right Bumper. "5": { - action: "forwardTab", - repeat: 1, - speed: 2.5 + action: "tabForward", + repeatRate: 0.5 }, // Select button. @@ -82,29 +79,25 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE 12: { action: "sendKey", key: "ArrowUp", - repeat: 1, - speed: 1 + repeatRate: 0.5 }, // Down 13: { action: "sendKey", key: "ArrowDown", - repeat: 1, - speed: 1 + repeatRate: 0.5 }, // Left 14: { action: "sendKey", key: "ArrowLeft", - repeat: 1, - speedFactor: 1 + repeatRate: 0.5 }, // Right. 15: { action: "sendKey", key: "ArrowRight", - repeat: 1, - speed: 1 + repeatRate: 0.5 }, // "Badge" button. @@ -113,18 +106,11 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE } }, axes: { - // Left thumbstick horizontal axis. - "0": { - action: "scrollHorizontally", - repeat: 1, - speed: 1, - invert: false - }, // Left thumbstick vertical axis. "1": { action: "scrollVertically", - repeat: 1, - speed: 1, + repeatRate: 0.15, + scrollFactor: 20, invert: false } } diff --git a/src/js/content_scripts/gamepad-navigator.js b/src/js/content_scripts/gamepad-navigator.js index 8c02565..d0d7edc 100644 --- a/src/js/content_scripts/gamepad-navigator.js +++ b/src/js/content_scripts/gamepad-navigator.js @@ -22,7 +22,10 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE // TODO: Figure out how this is used and how it differs from "in view"; connected: false, axes: {}, - buttons: {} + buttons: {}, + prefs: { + pollingFrequency: 50 + } }, events: { onGamepadConnected: null, @@ -34,8 +37,17 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE "onGamepadDisconnected.handleConnectedGamepads": "{that}.onDisconnected", "onDestroy.clearConnectivityInterval": "{that}.clearConnectivityInterval" }, + modelListeners: { + "prefs.pollingFrequency": { + funcName: "gamepad.navigator.setGamepadPollingInterval", + args: ["{that}"] + }, + "connected": { + funcName: "gamepad.navigator.setGamepadPollingInterval", + args: ["{that}"] + } + }, windowObject: window, - frequency: 50, members: { connectivityIntervalReference: null }, @@ -91,10 +103,17 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * */ gamepad.navigator.onConnected = function (that) { - // Store the gamepad info if no other gamepad is already connected. - if (!that.model.connected) { - // Scan the state of gamepad frequently. - that.connectivityIntervalReference = setInterval(that.pollGamepads, that.options.frequency); + that.applier.change("connected", true); + }; + + gamepad.navigator.setGamepadPollingInterval = function (that) { + clearInterval(that.connectivityIntervalReference); + + if (that.model.connected) { + var pollingFrequency = fluid.get(that.model, "prefs.pollingFrequency") || 50; + + // Poll the state of all connected gamepads. + that.connectivityIntervalReference = setInterval(that.pollGamepads, pollingFrequency); } }; @@ -140,21 +159,15 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE } } - /** - * If at least one gamepad is available then update the component's model - * as per the combined inputs. - */ - if (combinedGamepadData.connected) { - // Initiate the gamepad navigator model transaction. - var modelUpdateTransaction = that.applier.initiate(); + var modelUpdateTransaction = that.applier.initiate(); + modelUpdateTransaction.fireChangeRequest({ path: "connected", value: combinedGamepadData.connected }); - modelUpdateTransaction.fireChangeRequest({ path: "connected", value: combinedGamepadData.connected }); + if (combinedGamepadData.connected) { modelUpdateTransaction.fireChangeRequest({ path: "axes", value: combinedGamepadData.axes }); modelUpdateTransaction.fireChangeRequest({ path: "buttons", value: combinedGamepadData.buttons }); - - // Commit the current state of gamepad. - modelUpdateTransaction.commit(); } + + modelUpdateTransaction.commit(); }; /** @@ -167,9 +180,6 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * */ gamepad.navigator.onDisconnected = function (that) { - // Stop the interval loop scanning the gamepad state. - clearInterval(that.connectivityIntervalReference); - // Assume by default that no other gamepad is connected/available. var isGamepadAvailable = false; @@ -186,21 +196,14 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * component's model to its initial state. */ var modelUpdateTransaction = that.applier.initiate(); - modelUpdateTransaction.fireChangeRequest({ path: "connected", value: false }); - if (isGamepadAvailable) { - /** - * Commit the connected state as false for the onGamepadConnected event to - * work. - */ - modelUpdateTransaction.commit(); - that.events.onGamepadConnected.fire(); - } - else { + + modelUpdateTransaction.fireChangeRequest({ path: "connected", value: isGamepadAvailable }); + + if (!isGamepadAvailable) { modelUpdateTransaction.fireChangeRequest({ path: "axes", value: {} }); modelUpdateTransaction.fireChangeRequest({ path: "buttons", value: {} }); - - // Commit the initial model. - modelUpdateTransaction.commit(); } + + modelUpdateTransaction.commit(); }; })(fluid); diff --git a/src/js/content_scripts/input-mapper-background-utils.js b/src/js/content_scripts/input-mapper-background-utils.js index 21d8d88..d5cd935 100644 --- a/src/js/content_scripts/input-mapper-background-utils.js +++ b/src/js/content_scripts/input-mapper-background-utils.js @@ -20,17 +20,6 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE // TODO: Add continuous / long-press browser tab navigation, if needed. // TODO: Add browser tab navigation for thumbsticks. - /** - * TODO: Save the DOM element focus before switching tabs so that it can be restored - * when the user navigates back to the same tab. - */ - - gamepad.inputMapperUtils.background.postMessageOnControlDown = function (that, value, oldValue, actionOptions) { - if (oldValue === 0 && value > that.model.prefs.analogCutoff) { - gamepad.inputMapperUtils.background.postMessage(that, actionOptions); - } - }; - /** * * Connect to the background script, send a message, and handle the response. @@ -51,10 +40,10 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE var wrappedActionOptions = fluid.copy(actionOptions); - wrappedActionOptions.homepageURL = that.model.commonConfiguration.homepageURL; + wrappedActionOptions.newTabOrWindowURL = that.model.prefs.newTabOrWindowURL; // Set the left pixel if the action is about changing "window size". - if (actionOptions.actionName === "maximizeWindow" || actionOptions.actionName === "restoreWindowSize") { + if (actionOptions.action === "maximizeWindow" || actionOptions.action === "restoreWindowSize") { wrappedActionOptions.left = screen.availLeft; } @@ -67,26 +56,24 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * thumbsticks. * * @param {Object} that - The inputMapper component. - * @param {Integer} value - The value of the gamepad input. - * @param {Boolean} invert - Whether the zooming should be in opposite order. + * @param {Object} actionOptions - The parameters for this action. + * @property {Boolean} invert - Whether the zooming should be in opposite order. + * @param {String} inputType - The input type ("buttons" or "axes"). + * @param {String|Number} index - Which button number or axis we're responding to. * */ - gamepad.inputMapperUtils.background.thumbstickZoom = async function (that, value, invert) { - clearInterval(that.intervalRecords.zoomIn); - clearInterval(that.intervalRecords.zoomOut); + gamepad.inputMapperUtils.background.thumbstickZoom = async function (that, actionOptions, inputType, index) { + var inversionFactor = fluid.get(actionOptions, "invert") ? -1 : 1; + + var value = fluid.get(that.model, [inputType, index]); // Get the updated input value according to the configuration. - var inversionFactor = invert ? -1 : 1; var polarisedValue = value * inversionFactor; var zoomType = polarisedValue > 0 ? "zoomOut" : "zoomIn"; - var actionOptions = { actionName: zoomType }; - // Call the zoom changing invokers according to the input values. - if (Math.abs(value) > that.options.cutoffValue) { - that.intervalRecords[zoomType] = setInterval(function (actionOptions) { - gamepad.inputMapperUtils.background.postMessage(that, actionOptions); - }, that.options.frequency, actionOptions); - } + var delegatedActionOptions = fluid.copy(actionOptions); + delegatedActionOptions.action = zoomType; + gamepad.inputMapperUtils.background.postMessage(that, delegatedActionOptions); }; /** @@ -95,25 +82,27 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * thumbsticks. * * @param {Object} that - The inputMapper component. - * @param {Integer} value - The value of the gamepad input. - * @param {Boolean} invert - Whether the zooming should be in opposite order. + * @param {Object} actionOptions - The parameters for this action. + * @property {Boolean} invert - Whether the zooming should be in opposite order. + * @param {String} inputType - The input type ("buttons" or "axes"). + * @param {String|Number} index - Which button number or axis we're responding to. * */ - gamepad.inputMapperUtils.background.thumbstickWindowSize = function (that, value, invert) { + gamepad.inputMapperUtils.background.thumbstickWindowSize = function (that, actionOptions, inputType, index) { + var invert = fluid.get(actionOptions, "invert") || false; + + var value = fluid.get(that.model, [inputType, index]); + // Get the updated input value according to the configuration. var inversionFactor = invert ? -1 : 1; var polarisedValue = value * inversionFactor; - var actionName = polarisedValue > 0 ? "maximizeWindow" : "restoreWindowSize"; + var delegatedAction = polarisedValue > 0 ? "maximizeWindow" : "restoreWindowSize"; - var actionOptions = { - actionName: actionName, + var delegatedActionOptions = { + action: delegatedAction, left: screen.availLeft }; - - // Call the window size changing invokers according to the input value. - if (Math.abs(value) > that.options.cutoffValue) { - gamepad.inputMapperUtils.background.postMessage(that, actionOptions); - } + gamepad.inputMapperUtils.background.postMessage(that, delegatedActionOptions); }; })(fluid); diff --git a/src/js/content_scripts/input-mapper-base.js b/src/js/content_scripts/input-mapper-base.js index 26f09ce..c5d2ebf 100644 --- a/src/js/content_scripts/input-mapper-base.js +++ b/src/js/content_scripts/input-mapper-base.js @@ -24,7 +24,9 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE fluid.defaults("gamepad.inputMapper.base", { gradeNames: ["gamepad.configMaps", "gamepad.navigator"], model: { - pageInView: true + pageInView: true, + prefs: {}, + bindings: {} }, modelListeners: { "axes.*": { @@ -40,43 +42,14 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE "onDestroy.clearIntervalRecords": "{that}.clearIntervalRecords", "onCreate.trackDOM": "{that}.trackDOM", "onDestroy.stopTrackingDOM": "{that}.stopTrackingDOM", - /** - * TODO: Adjust the gamepaddisconnected event so that the other gamepad's - * navigation doesn't break. - */ "onGamepadDisconnected.clearIntervalRecords": "{that}.clearIntervalRecords" }, members: { - /** - * TODO: Move the member variables used for the inter-navigation web page - * features to the "inputMapper" component. - */ - // TODO: These should be expressed per control rather than per action, as we might bind an action to more than one control. - intervalRecords: { - upwardScroll: null, - downwardScroll: null, - leftScroll: null, - rightScroll: null, - forwardTab: null, - reverseTab: null, - zoomIn: null, - zoomOut: null, - ArrowLeft: null, - ArrowRight: null, - ArrowUp: null, - ArrowDown: null - }, + intervalRecords: {}, currentTabIndex: 0, tabbableElements: null, - mutationObserverInstance: null, - // TODO: Ensure that there are sensible defaults somewhere. - prefs: {}, - bindings: {} + mutationObserverInstance: null }, - // TODO: Make this configurable. - // "Jitter" cutoff Value for analog thumb sticks. - cutoffValue: 0.40, // TODO: Make this a preference - scrollInputMultiplier: 50, // TODO: Make this a preference invokers: { updateTabbables: { @@ -89,7 +62,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE }, clearIntervalRecords: { funcName: "gamepad.inputMapper.base.clearIntervalRecords", - args: ["{that}.intervalRecords"] + args: ["{that}"] }, trackDOM: { funcName: "gamepad.inputMapper.base.trackDOM", @@ -104,27 +77,27 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE args: ["{that}"] }, - // Actions are called with value, oldValue, actionOptions + // Actions are called with actionOptions, inputType, index click: { funcName: "gamepad.inputMapperUtils.content.click", - args: ["{that}", "{arguments}.0"] + args: ["{that}"] }, previousPageInHistory: { funcName: "gamepad.inputMapperUtils.content.previousPageInHistory", - args: ["{that}", "{arguments}.0"] + args: ["{that}"] }, nextPageInHistory: { funcName: "gamepad.inputMapperUtils.content.nextPageInHistory", - args: ["{that}", "{arguments}.0"] + args: ["{that}"] }, - reverseTab: { + tabBackward: { funcName: "gamepad.inputMapperUtils.content.buttonTabNavigation", - args: ["{that}", "{arguments}.0", "reverseTab"] + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] }, - forwardTab: { + tabForward: { funcName: "gamepad.inputMapperUtils.content.buttonTabNavigation", - args: ["{that}", "{arguments}.0", "forwardTab"] + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] }, scrollLeft: { @@ -153,105 +126,102 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE }, thumbstickHistoryNavigation: { funcName: "gamepad.inputMapperUtils.content.thumbstickHistoryNavigation", - args: ["{that}", "{arguments}.0", "{arguments}.2"] + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] }, // TODO: Add tests for when the number of tabbable elements changes. thumbstickTabbing: { funcName: "gamepad.inputMapperUtils.content.thumbstickTabbing", - args: ["{that}", "{arguments}.0", "{arguments}.2"] + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] }, sendKey: { funcName: "gamepad.inputMapperUtils.content.sendKey", - args: ["{that}", "{arguments}.0", "{arguments}.2"] // value, actionOptions + args: ["{that}", "{arguments}.0"] // actionOptions }, - // TODO: Remove these once we are using the new bindings instead of the old "map". - // Arrow actions for buttons - sendArrowLeft: { - funcName: "gamepad.inputMapperUtils.content.sendKey", - args: ["{that}", "{arguments}.0", { key: "ArrowLeft" }] // value, actionOptions - }, - sendArrowRight: { - funcName: "gamepad.inputMapperUtils.content.sendKey", - args: ["{that}", "{arguments}.0", { key: "ArrowRight" }] // value, actionOptions - }, - sendArrowUp: { - funcName: "gamepad.inputMapperUtils.content.sendKey", - args: ["{that}", "{arguments}.0", { key: "ArrowUp" }] // value, actionOptions - }, - sendArrowDown: { - funcName: "gamepad.inputMapperUtils.content.sendKey", - args: ["{that}", "{arguments}.0", { key: "ArrowDown" }] // value, actionOptions - }, // Arrow actions for axes thumbstickHorizontalArrows: { funcName: "gamepad.inputMapperUtils.content.thumbstickArrows", - args: ["{that}", "{arguments}.0", "{arguments}.2", "ArrowRight", "ArrowLeft"] // value, actionOptions, forwardKey, backwardKey + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "ArrowRight", "ArrowLeft"] // actionOptions, inputType, index, forwardKey, backwardKey }, thumbstickVerticalArrows: { funcName: "gamepad.inputMapperUtils.content.thumbstickArrows", - args: ["{that}", "{arguments}.0", "{arguments}.2", "ArrowDown", "ArrowUp"] // value, actionOptions, forwardKey, backwardKey + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "ArrowDown", "ArrowUp"] // actionOptions, inputType, index, forwardKey, backwardKey } } }); - /** - * TODO: Replace the "inputMapper" with "inputMapper.base" in the JSDoc comments for - * the invokers of "inputMapper.base" component. - */ - /** * - * Calls the invoker methods when axes/button is disturbed according to the - * configured action map to produce a navigation effect. + * Respond when the value of a bound button/axis changes. This function is now the sole arbiter of "discrete" vs. + * "continuous" actions, and is also the sole enforcer of the "analog cutoff". * * @param {Object} that - The inputMapper component. * @param {Object} change - The receipt for the change in input values. * */ gamepad.inputMapper.base.produceNavigation = function (that, change) { - // Only respond to gamepad input if we are "in view". - if (that.model.pageInView) { - /** - * Check if input is generated by axis or button and which button/axes was - * disturbed. - */ - var inputType = change.path[0], // i. e. "button", or "axis" - index = change.path[1], // i.e. 0, 1, 2 - inputValue = change.value, - oldInputValue = change.oldValue || 0; - - // Look for a binding at map.axis.0, map.button.1, et cetera. - var binding = that.model.map[inputType][index]; - // TODO: See how/whether we ever fail over using this structure. - var actionLabel = fluid.get(binding, "currentAction") || fluid.get(binding, "defaultAction"); - - /** - * TODO: Modify the action call in such a manner that the action gets triggered - * when the inputs are released. - * (To gain shortpress and longpress actions) - * - * Refer: - * https://github.com/fluid-lab/gamepad-navigator/pull/21#discussion_r453507050 - */ - - // Execute the actions only if the action label is available. - if (actionLabel) { - var action = fluid.get(that, actionLabel); - - // Trigger the action only if a valid function is found. - if (action) { - var actionOptions = fluid.copy(binding); - actionOptions.homepageURL = that.model.commonConfiguration.homepageURL; - - action( - inputValue, - oldInputValue, - actionOptions - ); + var inputType = change.path[0], // i. e. "button", or "axis" + index = change.path[1], // i.e. 0, 1, 2 + inputValue = change.value, + oldInputValue = change.oldValue || 0; + + var binding = fluid.get(that.model, ["bindings", inputType, index]); + if (binding) { + var action = fluid.get(binding, "action"); + var actionFn = fluid.get(that, action); + + var actionOptions = fluid.copy(binding); + + // Trigger the action only if a valid function is found. + if (actionFn) { + var intervalKey = gamepad.inputMapper.base.getIntervalKey(actionOptions, inputType, index); + + if (that.model.pageInView) { + if (action === "openNewTab" || action === "openNewWindow") { + actionOptions.newTabOrWindowURL = that.model.prefs.newTabOrWindowURL; + } + + var valueIsAboveCutoff = Math.abs(inputValue) > that.model.prefs.analogCutoff; + + if (valueIsAboveCutoff) { + // In response to the initial "down" event, perform the action immediately. + if (Math.abs(oldInputValue) < that.model.prefs.analogCutoff) { + // Always the first time. + actionFn( + actionOptions, + inputType, + index + ); + } + + var repeatRate = fluid.get(actionOptions, "repeatRate") || 0; + if (repeatRate) { + var repeatRateMs = repeatRate * 1000; + // For analog controls that fluctuate, we only want to start polling when they first + // cross the analog cutoff threshold. + if (!that.intervalRecords[intervalKey]) { + that.intervalRecords[intervalKey] = setInterval( + actionFn, + repeatRateMs, + actionOptions, inputType, index + ); + } + } + } + // clear the interval on button release. + else { + gamepad.inputMapper.base.clearInterval(that, intervalKey); + } + } + // clear the interval if our page is not in view. + else { + gamepad.inputMapper.base.clearInterval(that, intervalKey); } } + else { + fluid.log(fluid.logLevel.WARN, "Invalid binding for input type " + inputType + ", index " + index + ", no handler found for action '" + action + "'"); + } } }; @@ -260,15 +230,29 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * A listener for the input mapper component to clear the connectivity interval when * the instance of the component is destroyed. * - * @param {Object} records - The interval records object. + * @param {Object} that - The component whose interval records need to be cleared. * */ - gamepad.inputMapper.base.clearIntervalRecords = function (records) { - fluid.each(records, function (record) { - clearInterval(record); + gamepad.inputMapper.base.clearIntervalRecords = function (that) { + fluid.each(that.intervalRecords, function (intervalNumber, intervalKey) { + clearInterval(intervalNumber); + delete that.intervalRecords[intervalKey]; }); }; + gamepad.inputMapper.base.getIntervalKey = function (actionOptions, inputType, index) { + var action = fluid.get(actionOptions, "action"); + var intervalKey = [inputType, index, action].join("-"); + return intervalKey; + }; + + gamepad.inputMapper.base.clearInterval = function (that, intervalKey) { + if (that.intervalRecords[intervalKey]) { + clearInterval(that.intervalRecords[intervalKey]); + delete that.intervalRecords[intervalKey]; + } + }; + gamepad.inputMapper.base.updateTabbables = function (that) { that.tabbableElements = ally.query.tabsequence({ strategy: "strict" }); }; diff --git a/src/js/content_scripts/input-mapper-content-utils.js b/src/js/content_scripts/input-mapper-content-utils.js index 6c8a05d..995fb8e 100644 --- a/src/js/content_scripts/input-mapper-content-utils.js +++ b/src/js/content_scripts/input-mapper-content-utils.js @@ -17,38 +17,26 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE fluid.registerNamespace("gamepad.inputMapperUtils.content"); - /** - * TODO: Fix the "speedFactor" usage in invokers to reduce the given interval loop - * frequency. - */ - /** * * Scroll horizontally across the webpage. * * @param {Object} that - The inputMapper component. - * @param {Integer} value - The current value of the gamepad input. - * @param {Integer} oldValue - The previous value of the gamepad input. - * @param {Object} actionOptions - The action options (ex: speedFactor, invert). + * @param {Object} actionOptions - The action options. + * @property {Boolean} invert - Whether to invert the direction of scroll. + * @param {String} inputType - The input type ("buttons" or "axes"). + * @param {String|Number} index - Which button number or axis we're responding to. * */ - gamepad.inputMapperUtils.content.scrollHorizontally = function (that, value, oldValue, actionOptions) { - if (that.model.pageInView) { - // Get the updated input value according to the configuration. - var inversionFactor = actionOptions.invert ? -1 : 1; - value = value * inversionFactor; - if (value > 0) { - clearInterval(that.intervalRecords.leftScroll); - that.scrollRight(value, oldValue, actionOptions); - } - else if (value < 0) { - clearInterval(that.intervalRecords.rightScroll); - that.scrollLeft(-1 * value, oldValue, actionOptions); - } - else { - clearInterval(that.intervalRecords.leftScroll); - clearInterval(that.intervalRecords.rightScroll); - } + gamepad.inputMapperUtils.content.scrollHorizontally = function (that, actionOptions, inputType, index) { + var value = fluid.get(that.model, [inputType, index]); + var inversionFactor = fluid.get(actionOptions, "invert") ? -1 : 1; + var polarisedValue = value * inversionFactor; + if (polarisedValue > 0) { + that.scrollRight(actionOptions, inputType, index); + } + else if (polarisedValue < 0) { + that.scrollLeft(actionOptions, inputType, index); } }; @@ -57,36 +45,24 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Scroll the webpage in left direction. * * @param {Object} that - The inputMapper component. - * @param {Integer} value - The current value of the gamepad input. - * @param {Integer} oldValue - The previous value of the gamepad input. - * @param {Object} actionOptions - The action options (ex: speedFactor). + * @param {Object} actionOptions - The action options (ex: scrollFactor). + * @property {Number} scrollFactor - How far to scroll in a single pass (from 1 to 50). + * @param {String} inputType - The input type ("buttons" or "axes"). + * @param {String|Number} index - Which button number or axis we're responding to. * */ - gamepad.inputMapperUtils.content.scrollLeft = function (that, value, oldValue, actionOptions) { - var speedFactor = actionOptions.speedFactor || 1; - - /** - * Stop scrolling for the previous input value. Also stop scrolling if the input - * source (analog/button) is at rest. - */ - clearInterval(that.intervalRecords.leftScroll); - - /** - * Scroll the webpage towards the left only if the input value is more than the - * cutoff value. - */ - if (that.model.pageInView && (value > that.options.cutoffValue)) { - // Scroll to the left according to the new input value. - that.intervalRecords.leftScroll = setInterval(function () { - if (window.scrollX > 0) { - window.scroll(window.scrollX - value * that.options.scrollInputMultiplier * speedFactor, window.scrollY); - } - else { - clearInterval(that.intervalRecords.leftScroll); - that.vibrate(); - } + gamepad.inputMapperUtils.content.scrollLeft = function (that, actionOptions, inputType, index) { + var value = fluid.get(that.model, [inputType, index]); + var scrollFactor = fluid.get(actionOptions, "scrollFactor") || 1; - }, that.options.frequency); + // Scroll to the left according to the new input value. + if (window.scrollX > 0) { + window.scroll(window.scrollX - value * that.options.scrollInputMultiplier * scrollFactor, window.scrollY); + } + else { + var intervalKey = gamepad.inputMapper.base.getIntervalKey(actionOptions, inputType, index); + gamepad.inputMapper.base.clearInterval(that, intervalKey); + that.vibrate(); } }; @@ -95,36 +71,26 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Scroll the webpage towards the right direction. * * @param {Object} that - The inputMapper component. - * @param {Integer} value - The current value of the gamepad input. - * @param {Integer} oldValue - The previous value of the gamepad input. - * @param {Object} actionOptions - The action options (ex: speedFactor). + * @param {Object} actionOptions - The action options. + * @property {Number} scrollFactor - How far to scroll in a single pass (from 1 to 50). + * @param {String} inputType - The input type ("buttons" or "axes"). + * @param {String|Number} index - Which button number or axis we're responding to. * */ - gamepad.inputMapperUtils.content.scrollRight = function (that, value, oldValue, actionOptions) { - var speedFactor = actionOptions.speedFactor || 1; - - /** - * Stop scrolling for the previous input value. Also stop scrolling if the input - * source (analog/button) is at rest. - */ - clearInterval(that.intervalRecords.rightScroll); - - /** - * Scroll the webpage towards the right only if the input value is more than the - * cutoff value. - */ - if (that.model.pageInView && (value > that.options.cutoffValue)) { - // Scroll to the right according to the new input value. - that.intervalRecords.rightScroll = setInterval(function () { - window.scroll(window.scrollX + value * that.options.scrollInputMultiplier * speedFactor, window.scrollY); - - var documentWidth = document.body.scrollWidth; - var currentScrollX = window.scrollX + window.innerWidth; - if (currentScrollX >= documentWidth) { - clearInterval(that.intervalRecords.rightScroll); - that.vibrate(); - } - }, that.options.frequency); + gamepad.inputMapperUtils.content.scrollRight = function (that, actionOptions, inputType, index) { + var scrollFactor = fluid.get(actionOptions, "scrollFactor") || 1; + var value = fluid.get(that.model, [inputType, index]); + + + // Scroll to the right according to the new input value. + window.scroll(window.scrollX + value * that.options.scrollInputMultiplier * scrollFactor, window.scrollY); + + var documentWidth = document.body.scrollWidth; + var currentScrollX = window.scrollX + window.innerWidth; + if (currentScrollX >= documentWidth) { + var intervalKey = gamepad.inputMapper.base.getIntervalKey(actionOptions, inputType, index); + gamepad.inputMapper.base.clearInterval(that, intervalKey); + that.vibrate(); } }; @@ -133,30 +99,21 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Scroll vertically across the webpage. * * @param {Object} that - The inputMapper component. - * @param {Integer} value - The currrent value of the gamepad input. - * @param {Integer} oldValue - The previous value of the gamepad input. - * @param {Object} actionOptions - The action options (ex: speedFactor). + * @param {Object} actionOptions - The action options. + * @param {String} inputType - The input type ("buttons" or "axes"). + * @param {String|Number} index - Which button number or axis we're responding to. * */ - gamepad.inputMapperUtils.content.scrollVertically = function (that, value, oldValue, actionOptions) { - var speedFactor = actionOptions.speedFactor || 1; - - if (that.model.pageInView) { - // Get the updated input value according to the configuration. - var inversionFactor = actionOptions.invert ? -1 : 1; - value = value * inversionFactor; - if (value > 0) { - clearInterval(that.intervalRecords.upwardScroll); - that.scrollDown(value, oldValue, speedFactor); - } - else if (value < 0) { - clearInterval(that.intervalRecords.downwardScroll); - that.scrollUp(-1 * value, oldValue, speedFactor); - } - else { - clearInterval(that.intervalRecords.upwardScroll); - clearInterval(that.intervalRecords.downwardScroll); - } + gamepad.inputMapperUtils.content.scrollVertically = function (that, actionOptions, inputType, index) { + var inversionFactor = fluid.get(actionOptions, "invert") ? -1 : 1; + var value = fluid.get(that.model, [inputType, index]); + + var polarisedValue = value * inversionFactor; + if (polarisedValue > 0) { + that.scrollDown(actionOptions, inputType, index); + } + else if (polarisedValue < 0) { + that.scrollUp(actionOptions, inputType, index); } }; @@ -165,35 +122,25 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Scroll the webpage in upward direction. * * @param {Object} that - The inputMapper component. - * @param {Integer} value - The current value of the gamepad input. - * @param {Integer} oldValue - The previous value of the gamepad input. - * @param {Object} actionOptions - The action options (ex: speedFactor). + * @param {Object} actionOptions - The action options. + * @property {Number} scrollFactor - The amount (from 1 to 50) to scroll in a single pass. + * @param {String} inputType - The input type ("buttons" or "axes"). + * @param {String|Number} index - Which button number or axis we're responding to. * */ - gamepad.inputMapperUtils.content.scrollUp = function (that, value, oldValue, actionOptions) { - var speedFactor = actionOptions.speedFactor || 1; - - /** - * Stop scrolling for the previous input value. Also stop scrolling if the input - * source (analog/button) is at rest. - */ - clearInterval(that.intervalRecords.upwardScroll); - - /** - * Scroll the webpage upward only if the input value is more than the cutoff - * value. - */ - if (that.model.pageInView && (value > that.options.cutoffValue)) { - // Scroll upward according to the new input value. - that.intervalRecords.upwardScroll = setInterval(function () { - if (window.scrollY > 0) { - window.scroll(window.scrollX, window.scrollY - value * that.options.scrollInputMultiplier * speedFactor); - } - else { - clearInterval(that.intervalRecords.upwardScroll); - that.vibrate(); - } - }, that.options.frequency); + gamepad.inputMapperUtils.content.scrollUp = function (that, actionOptions, inputType, index) { + var scrollFactor = fluid.get(actionOptions, "scrollFactor") || 1; + + var value = Math.abs(fluid.get(that.model, [inputType, index])); + + if (window.scrollY > 0) { + window.scroll(window.scrollX, window.scrollY - (value * scrollFactor)); + } + else { + var intervalKey = gamepad.inputMapper.base.getIntervalKey(actionOptions, inputType, index); + gamepad.inputMapper.base.clearInterval(that, intervalKey); + + that.vibrate(); } }; @@ -202,39 +149,30 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Scroll the webpage in downward direction. * * @param {Object} that - The inputMapper component. - * @param {Integer} value - The current value of the gamepad input. - * @param {Integer} oldValue - The previous value of the gamepad input. - * @param {Object} actionOptions - The action options (ex: speedFactor). + * @param {Object} actionOptions - The action options. + * @property {Number} scrollFactor - The amount (from 1 to 50) to scroll in a single pass. + * @param {String} inputType - The input type ("buttons" or "axes"). + * @param {String|Number} index - Which button number or axis we're responding to. * */ - gamepad.inputMapperUtils.content.scrollDown = function (that, value, oldValue, actionOptions) { - var speedFactor = actionOptions.speedFactor || 1; - - /** - * Stop scrolling for the previous input value. Also stop scrolling if the input - * source (analog/button) is at rest. - */ - clearInterval(that.intervalRecords.downwardScroll); - - /** - * Scroll the webpage downward only if the input value is more than the cutoff - * value. - */ - if (that.model.pageInView && (value > that.options.cutoffValue)) { - // Scroll upward according to the new input value. - that.intervalRecords.downwardScroll = setInterval(function () { - window.scroll(window.scrollX, window.scrollY + value * that.options.scrollInputMultiplier * speedFactor); - - // Adapted from: - // https://fjolt.com/article/javascript-check-if-user-scrolled-to-bottom - var documentHeight = document.body.scrollHeight; - var currentScroll = window.scrollY + window.innerHeight; - if (currentScroll >= documentHeight) { - clearInterval(that.intervalRecords.downwardScroll); - that.vibrate(); - }; - }, that.options.frequency); - } + gamepad.inputMapperUtils.content.scrollDown = function (that, actionOptions, inputType, index) { + var scrollFactor = fluid.get(actionOptions, "scrollFactor") || 1; + + var value = Math.abs(fluid.get(that.model, [inputType, index])); + + // Scroll upward according to the new input value. + window.scroll(window.scrollX, window.scrollY + (value * scrollFactor)); + + // Adapted from: + // https://fjolt.com/article/javascript-check-if-user-scrolled-to-bottom + var documentHeight = document.body.scrollHeight; + // We add a little wiggle here, as window.innerHeight is a float that is a bit short of the total height. + var currentScroll = (window.scrollY + window.innerHeight + 1); + if (currentScroll >= documentHeight) { + var intervalKey = gamepad.inputMapper.base.getIntervalKey(actionOptions, inputType, index); + gamepad.inputMapper.base.clearInterval(that, intervalKey); + that.vibrate(); + }; }; /** @@ -242,32 +180,16 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Tab through the webpage using thumbsticks. * * @param {Object} that - The inputMapper component. - * @param {Integer} value - The value of the gamepad input. - * @param {Object} actionOptions - The action options (ex: speedFactor). + * @param {Object} actionOptions - The action options. + * @param {String} inputType - The input type ("buttons" or "axes"). + * @param {String|Number} index - Which button number or axis we're responding to. * */ - gamepad.inputMapperUtils.content.thumbstickTabbing = function (that, value, actionOptions) { - if (that.model.pageInView) { - var speedFactor = actionOptions.speedFactor || 1; - var inversionFactor = actionOptions.invert ? -1 : 1; - value = value * inversionFactor; - clearInterval(that.intervalRecords.forwardTab); - clearInterval(that.intervalRecords.reverseTab); - if (value > 0) { - that.intervalRecords.forwardTab = setInterval( - that.forwardTab, - that.options.frequency * speedFactor, - value - ); - } - else if (value < 0) { - that.intervalRecords.reverseTab = setInterval( - that.reverseTab, - that.options.frequency * speedFactor, - -1 * value - ); - } - } + gamepad.inputMapperUtils.content.thumbstickTabbing = function (that, actionOptions, inputType, index) { + var value = fluid.get(that.model, [inputType, index]); + var delegatedActionOptions = fluid.copy(actionOptions); + delegatedActionOptions.action = value > 0 ? "tabForward" : "tabBackward"; + gamepad.inputMapperUtils.content.buttonTabNavigation(that, delegatedActionOptions, inputType, index); }; /** @@ -275,56 +197,58 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Change the focus from one tabbable element to another. * * @param {Object} that - The inputMapper component. - * @param {Integer} value - The value of the gamepad input. - * @param {String} direction - The direction in which the focus should change. + * @param {Object} actionOptions - The action options. + * @property {Boolean} invert - Whether to invert the direction in which we navigate. + * @property {Number} repeatRate - How often (in seconds) to repeat the action. + * @param {String} inputType - The input type ("buttons" or "axes"). + * @param {String|Number} index - Which button number or axis we're responding to. * */ - gamepad.inputMapperUtils.content.buttonTabNavigation = function (that, value, direction) { - if (that.model.pageInView && (value > that.options.cutoffValue)) { - var length = that.tabbableElements.length; + gamepad.inputMapperUtils.content.buttonTabNavigation = function (that, actionOptions, inputType, index) { + var inversionFactor = fluid.get(actionOptions, "invert") ? -1 : 1; + var value = fluid.get(that.model, [inputType, index]); + + var length = that.tabbableElements.length; + + // Tab only if at least one tabbable element is available. + if (length) { + /** + * If the body element of the page is focused or if no element is + * currently focused, shift the focus to the first element. Otherwise + * shift the focus to the next element. + */ + var activeElement = that.model.activeModal ? fluid.get(that, "model.shadowElement.activeElement") : document.activeElement; + if (activeElement.nodeName === "BODY" || !activeElement) { + that.tabbableElements[0].focus(); + } + else { + var activeElementIndex = that.tabbableElements.indexOf(activeElement); - // Tab only if at least one tabbable element is available. - if (length) { /** - * If the body element of the page is focused or if no element is - * currently focused, shift the focus to the first element. Otherwise - * shift the focus to the next element. + * If the currently focused element is not found in the list, refer to + * the stored value of the index. */ - var activeElement = that.model.activeModal ? fluid.get(that, "model.shadowElement.activeElement") : document.activeElement; - if (activeElement.nodeName === "BODY" || !activeElement) { - that.tabbableElements[0].focus(); + if (activeElementIndex === -1) { + activeElementIndex = that.currentTabIndex; } - else { - var activeElementIndex = that.tabbableElements.indexOf(activeElement); - /** - * If the currently focused element is not found in the list, refer to - * the stored value of the index. - */ - if (activeElementIndex === -1) { - activeElementIndex = that.currentTabIndex; - } + activeElement.blur(); - var increment = 0; - if (direction === "forwardTab") { - increment = 1; - } - else if (direction === "reverseTab") { - increment = -1; - } + var actionPolarity = actionOptions.action === "tabForward" ? 1 : -1; - activeElement.blur(); + var fullyWeightedValue = value * actionPolarity * inversionFactor; + var increment = fullyWeightedValue > 0 ? 1 : -1; - that.currentTabIndex = (that.tabbableElements.length + (activeElementIndex + increment)) % that.tabbableElements.length; - var elementToFocus = that.tabbableElements[that.currentTabIndex]; - elementToFocus.focus(); - // If focus didn't succeed, make one more attempt, to attempt to avoid focus traps (See #118). - if (!that.model.activeModal && elementToFocus !== document.activeElement) { - that.currentTabIndex = (that.tabbableElements.length + (that.currentTabIndex + increment)) % that.tabbableElements.length; - var failoverElementToFocus = that.tabbableElements[that.currentTabIndex]; - failoverElementToFocus.focus(); - } + that.currentTabIndex = (that.tabbableElements.length + (activeElementIndex + increment)) % that.tabbableElements.length; + var elementToFocus = that.tabbableElements[that.currentTabIndex]; + elementToFocus.focus(); + + // If focus didn't succeed, make one more attempt, to attempt to avoid focus traps (See #118). + if (!that.model.activeModal && elementToFocus !== document.activeElement) { + that.currentTabIndex = (that.tabbableElements.length + (that.currentTabIndex + increment)) % that.tabbableElements.length; + var failoverElementToFocus = that.tabbableElements[that.currentTabIndex]; + failoverElementToFocus.focus(); } } } @@ -335,71 +259,69 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Click on the currently focused element. * * @param {Object} that - The inputMapper component. - * @param {Integer} value - The value of the gamepad input. * */ - gamepad.inputMapperUtils.content.click = function (that, value) { - if (that.model.pageInView && (value > 0)) { - var activeElement = that.model.activeModal ? fluid.get(that, "model.shadowElement.activeElement") : document.activeElement; + gamepad.inputMapperUtils.content.click = function (that) { + var activeElement = that.model.activeModal ? fluid.get(that, "model.shadowElement.activeElement") : document.activeElement; - if (activeElement) { - var isTextInput = gamepad.inputMapperUtils.content.isTextInput(activeElement); + if (activeElement) { + var isTextInput = gamepad.inputMapperUtils.content.isTextInput(activeElement); - // Open the new onscreen keyboard to input text. - if (isTextInput) { - var lastExternalFocused = activeElement; - that.applier.change("lastExternalFocused", lastExternalFocused); - that.applier.change("textInputValue", lastExternalFocused.value); - lastExternalFocused.blur(); + // Open the new onscreen keyboard to input text. + if (isTextInput) { + var lastExternalFocused = activeElement; + that.applier.change("lastExternalFocused", lastExternalFocused); + that.applier.change("textInputValue", lastExternalFocused.value); + lastExternalFocused.blur(); - that.applier.change("activeModal", "onscreenKeyboard"); - } - /** - * If SELECT element is currently focused, toggle its state. Otherwise perform - * the regular click operation. - */ - else if (activeElement.nodeName === "SELECT") { - var optionsLength = 0; - - // Compute the number of options and store it. - activeElement.childNodes.forEach(function (childNode) { - if (childNode.nodeName === "OPTION") { - optionsLength++; - } - }); - - // Toggle the SELECT dropdown. - if (!activeElement.getAttribute("size") || activeElement.getAttribute("size") === "1") { - /** - * Store the initial size of the dropdown in a separate attribute - * (if specified already). - */ - var initialSizeString = activeElement.getAttribute("size"); - if (initialSizeString) { - activeElement.setAttribute("initialSize", parseInt(initialSizeString)); - } - - /** - * Allow limited expansion to avoid an overflowing list, considering the - * list could go as large as 100 or more (for example, a list of - * countries). - */ - var length = Math.min(15, optionsLength); - activeElement.setAttribute("size", length); + that.applier.change("activeModal", "onscreenKeyboard"); + } + /** + * If SELECT element is currently focused, toggle its state. Otherwise perform + * the regular click operation. + */ + else if (activeElement.nodeName === "SELECT") { + // TODO: Replace this with a new modal. + var optionsLength = 0; + + // Compute the number of options and store it. + activeElement.childNodes.forEach(function (childNode) { + if (childNode.nodeName === "OPTION") { + optionsLength++; } - else { - // Obtain the initial size of the dropdown. - var sizeString = activeElement.getAttribute("initialSize") || "1"; + }); - // Restore the size of the dropdown. - activeElement.setAttribute("size", parseInt(sizeString)); + // Toggle the SELECT dropdown. + if (!activeElement.getAttribute("size") || activeElement.getAttribute("size") === "1") { + /** + * Store the initial size of the dropdown in a separate attribute + * (if specified already). + */ + var initialSizeString = activeElement.getAttribute("size"); + if (initialSizeString) { + activeElement.setAttribute("initialSize", parseInt(initialSizeString)); } + + /** + * Allow limited expansion to avoid an overflowing list, considering the + * list could go as large as 100 or more (for example, a list of + * countries). + */ + var length = Math.min(15, optionsLength); + activeElement.setAttribute("size", length); } else { - // Click on the focused element. - activeElement.click(); + // Obtain the initial size of the dropdown. + var sizeString = activeElement.getAttribute("initialSize") || "1"; + + // Restore the size of the dropdown. + activeElement.setAttribute("size", parseInt(sizeString)); } } + else { + // Click on the focused element. + activeElement.click(); + } } }; @@ -423,21 +345,21 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Navigate to the previous/next page in history using thumbsticks. * * @param {Object} that - The inputMapper component. - * @param {Integer} value - The value of the gamepad input. * @param {Object} actionOptions - The action options (ex: invert). + * @property {Boolean} invert - Whether to invert the direction of motion. + * @param {String} inputType - The input type ("buttons" or "axes"). + * @param {String|Number} index - Which button number or axis we're responding to. * */ - gamepad.inputMapperUtils.content.thumbstickHistoryNavigation = function (that, value, actionOptions) { - if (that.model.pageInView) { - // Get the updated input value according to the configuration. - var inversionFactor = actionOptions.invert ? -1 : 1; - value = value * inversionFactor; - if (value > 0) { - that.nextPageInHistory(value); - } - else if (value < 0) { - that.previousPageInHistory(-1 * value); - } + gamepad.inputMapperUtils.content.thumbstickHistoryNavigation = function (that, actionOptions, inputType, index) { + var inversionFactor = fluid.get(actionOptions, "invert") ? -1 : 1; + var value = fluid.get(that.model, [inputType, index]); + var polarisedValue = value * inversionFactor; + if (polarisedValue > 0) { + that.nextPageInHistory(); + } + else if (polarisedValue < 0) { + that.previousPageInHistory(); } }; @@ -451,36 +373,33 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Navigate to the previous page in history. * * @param {Object} that - The inputMapper component. - * @param {Integer} value - The value of the gamepad input. * */ - gamepad.inputMapperUtils.content.previousPageInHistory = function (that, value) { - if (that.model.pageInView && (value > that.options.cutoffValue)) { - if (window.history.length > 1) { - var activeElementIndex = null; - - // Get the index of the currently active element, if available. - if (fluid.get(document, "activeElement")) { - var tabbableElements = ally.query.tabsequence({ strategy: "strict" }); - activeElementIndex = tabbableElements.indexOf(document.activeElement); - } - - /** - * Store the index of the active element in local storage object with its key - * set to the URL of the webpage and navigate back in history. - */ - var storageData = {}, - pageAddress = that.options.windowObject.location.href; - if (activeElementIndex !== -1) { - storageData[pageAddress] = activeElementIndex; - } - chrome.storage.local.set(storageData, function () { - that.options.windowObject.history.back(); - }); + gamepad.inputMapperUtils.content.previousPageInHistory = function (that) { + if (window.history.length > 1) { + var activeElementIndex = null; + + // Get the index of the currently active element, if available. + if (fluid.get(document, "activeElement")) { + var tabbableElements = ally.query.tabsequence({ strategy: "strict" }); + activeElementIndex = tabbableElements.indexOf(document.activeElement); } - else { - that.vibrate(); + + /** + * Store the index of the active element in local storage object with its key + * set to the URL of the webpage and navigate back in history. + */ + var storageData = {}, + pageAddress = that.options.windowObject.location.href; + if (activeElementIndex !== -1) { + storageData[pageAddress] = activeElementIndex; } + chrome.storage.local.set(storageData, function () { + that.options.windowObject.history.back(); + }); + } + else { + that.vibrate(); } }; @@ -489,36 +408,33 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Navigate to the next page in history. * * @param {Object} that - The inputMapper component. - * @param {Integer} value - The value of the gamepad input. * */ - gamepad.inputMapperUtils.content.nextPageInHistory = function (that, value) { - if (that.model.pageInView && (value > that.options.cutoffValue)) { - if (window.history.length > 1) { - var activeElementIndex = null; - - // Get the index of the currently active element, if available. - if (fluid.get(document, "activeElement")) { - var tabbableElements = ally.query.tabsequence({ strategy: "strict" }); - activeElementIndex = tabbableElements.indexOf(document.activeElement); - } - - /** - * Store the index of the active element in local storage object with its key - * set to the URL of the webpage and navigate forward in history. - */ - var storageData = {}, - pageAddress = that.options.windowObject.location.href; - if (activeElementIndex !== -1) { - storageData[pageAddress] = activeElementIndex; - } - chrome.storage.local.set(storageData, function () { - that.options.windowObject.history.forward(); - }); + gamepad.inputMapperUtils.content.nextPageInHistory = function (that) { + if (window.history.length > 1) { + var activeElementIndex = null; + + // Get the index of the currently active element, if available. + if (fluid.get(document, "activeElement")) { + var tabbableElements = ally.query.tabsequence({ strategy: "strict" }); + activeElementIndex = tabbableElements.indexOf(document.activeElement); } - else { - that.vibrate(); + + /** + * Store the index of the active element in local storage object with its key + * set to the URL of the webpage and navigate forward in history. + */ + var storageData = {}, + pageAddress = that.options.windowObject.location.href; + if (activeElementIndex !== -1) { + storageData[pageAddress] = activeElementIndex; } + chrome.storage.local.set(storageData, function () { + that.options.windowObject.history.forward(); + }); + } + else { + that.vibrate(); } }; @@ -526,28 +442,23 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * * Simulate a key press (down and up) on the current focused element. * @param {Object} that - The inputMapper component. - * @param {Number} value - The current value of the input (from 0 to 1). * @param {Object} actionOptions - The options for this action. * @property {String} key - The key (ex: `ArrowLeft`) to simulate. * */ - gamepad.inputMapperUtils.content.sendKey = function (that, value, actionOptions) { + gamepad.inputMapperUtils.content.sendKey = function (that, actionOptions) { var key = fluid.get(actionOptions, "key"); + var activeElement = that.model.activeModal ? fluid.get(that, "model.shadowElement.activeElement") : document.activeElement; - // TODO: Make this use the "analogCutoff" preference. - if (that.model.pageInView && (value > that.options.cutoffValue) && (key !== undefined)) { - var activeElement = that.model.activeModal ? fluid.get(that, "model.shadowElement.activeElement") : document.activeElement; - - if (activeElement) { - var keyDownEvent = new KeyboardEvent("keydown", { key: key, code: key, bubbles: true }); - activeElement.dispatchEvent(keyDownEvent); + if (activeElement) { + var keyDownEvent = new KeyboardEvent("keydown", { key: key, code: key, bubbles: true }); + activeElement.dispatchEvent(keyDownEvent); - // TODO: Test with text inputs and textarea fields to see if - // beforeinput and input are needed. + // TODO: Test with text inputs and textarea fields to see if + // beforeinput and input are needed. - var keyUpEvent = new KeyboardEvent("keyup", { key: key, code: key, bubbles: true }); - activeElement.dispatchEvent(keyUpEvent); - } + var keyUpEvent = new KeyboardEvent("keyup", { key: key, code: key, bubbles: true }); + activeElement.dispatchEvent(keyUpEvent); } }; @@ -556,35 +467,25 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE * Move through the webpage by sending arrow keys to the focused element. * * @param {Object} that - The inputMapper component. - * @param {Integer} value - The value of the gamepad input. - * @param {Object} actionOptions - The action options (ex: speedFactor). + * @param {Object} actionOptions - The action options. + * @property {Boolean} invert - Whether to invert the direction of motion. + * @param {String} inputType - The input type ("buttons" or "axes"). + * @param {String|Number} index - Which button number or axis we're responding to. * @param {String} forwardKey - The key/code for the forward arrow (right or down). * @param {String} backwardKey - The key/code for the backward arrow (left or up). * */ - gamepad.inputMapperUtils.content.thumbstickArrows = function (that, value, actionOptions, forwardKey, backwardKey) { - var speedFactor = actionOptions.speedFactor || 1; - var inversionFactor = actionOptions.invert ? -1 : 1; - value = value * inversionFactor; - clearInterval(that.intervalRecords[forwardKey]); - clearInterval(that.intervalRecords[backwardKey]); - if (value > that.options.cutoffValue) { - that.intervalRecords[forwardKey] = setInterval( - gamepad.inputMapperUtils.content.sendKey, // func - that.options.frequency * speedFactor, // delay - that, // arg 0 - value, //arg 1 - { key: forwardKey } // arg 2 - ); + gamepad.inputMapperUtils.content.thumbstickArrows = function (that, actionOptions, inputType, index, forwardKey, backwardKey) { + var inversionFactor = fluid.get(actionOptions, "invert") ? -1 : 1; + + var value = fluid.get(that.model, [inputType, index]); + var directionalValue = value * inversionFactor; + + if (directionalValue > 0) { + gamepad.inputMapperUtils.content.sendKey(that, { key: forwardKey }); } - else if (value < (-1 * that.options.cutoffValue)) { - that.intervalRecords[backwardKey] = setInterval( - gamepad.inputMapperUtils.content.sendKey, - that.options.frequency * speedFactor, - that, - -1 * value, - { key: backwardKey } - ); + else { + gamepad.inputMapperUtils.content.sendKey(that, { key: backwardKey }); } }; })(fluid, jQuery); diff --git a/src/js/content_scripts/input-mapper.js b/src/js/content_scripts/input-mapper.js index cd3cc32..0ec4e96 100644 --- a/src/js/content_scripts/input-mapper.js +++ b/src/js/content_scripts/input-mapper.js @@ -51,13 +51,6 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE }, listeners: { - // Old persistence. - // TODO: Remove - "onCreate.updateControls": { - funcName: "gamepad.inputMapper.updateControls", - args: ["{that}"] - }, - "onCreate.loadSettings": { funcName: "gamepad.inputMapper.loadSettings", args: ["{that}"] @@ -100,78 +93,82 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE } }, invokers: { - // Actions, these are called with: value, oldValue, actionOptions + // Actions, these are called with: actionOptions, inputType, index goToPreviousTab: { - funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", - args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "goToPreviousTab" }] + funcName: "gamepad.inputMapperUtils.background.postMessage", + args: ["{that}", { action: "goToPreviousTab" }] }, goToNextTab: { - funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", - args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "goToNextTab"}] + funcName: "gamepad.inputMapperUtils.background.postMessage", + args: ["{that}", { action: "goToNextTab"}] }, closeCurrentTab: { - funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", - args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "closeCurrentTab"}] + funcName: "gamepad.inputMapperUtils.background.postMessage", + args: ["{that}",{ action: "closeCurrentTab"}] }, openNewTab: { - funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", - args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "openNewTab" }] + funcName: "gamepad.inputMapperUtils.background.postMessage", + args: ["{that}", { action: "openNewTab" }] }, openNewWindow: { - funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", - args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "openNewWindow" }] + funcName: "gamepad.inputMapperUtils.background.postMessage", + args: ["{that}", { action: "openNewWindow" }] }, closeCurrentWindow: { - funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", - args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "closeCurrentWindow" }] + funcName: "gamepad.inputMapperUtils.background.postMessage", + args: ["{that}", { action: "closeCurrentWindow" }] }, goToPreviousWindow: { - funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", - args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "goToPreviousWindow" }] + funcName: "gamepad.inputMapperUtils.background.postMessage", + args: ["{that}", { action: "goToPreviousWindow" }] }, goToNextWindow: { - funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", - args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "goToNextWindow" }] + funcName: "gamepad.inputMapperUtils.background.postMessage", + args: ["{that}", { action: "goToNextWindow" }] }, zoomIn: { - funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", - args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "zoomIn" }] + funcName: "gamepad.inputMapperUtils.background.postMessage", + args: ["{that}", { action: "zoomIn" }] }, zoomOut: { - funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", - args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "zoomOut" }] - }, - thumbstickZoom: { - funcName: "gamepad.inputMapperUtils.background.thumbstickZoom", - args: ["{that}", "{arguments}.0", "{arguments}.2"] + funcName: "gamepad.inputMapperUtils.background.postMessage", + args: ["{that}", { action: "zoomOut" }] }, + + maximizeWindow: { - funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", - args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "maximizeWindow" }] + funcName: "gamepad.inputMapperUtils.background.postMessage", + args: ["{that}", { action: "maximizeWindow" }] }, restoreWindowSize: { - funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", - args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "restoreWindowSize" }] + funcName: "gamepad.inputMapperUtils.background.postMessage", + args: ["{that}", { action: "restoreWindowSize" }] + }, + reopenTabOrWindow: { + funcName: "gamepad.inputMapperUtils.background.postMessage", + args: ["{that}", { action: "reopenTabOrWindow" }] + }, + + thumbstickZoom: { + funcName: "gamepad.inputMapperUtils.background.thumbstickZoom", + args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"] }, thumbstickWindowSize: { funcName: "gamepad.inputMapperUtils.background.thumbstickWindowSize", args: ["{that}", "{arguments}.0", "{arguments}.2"] // value, actionOptions }, - reopenTabOrWindow: { - funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown", - args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "reopenTabOrWindow" }] - }, + openActionLauncher: { funcName: "gamepad.inputMapper.openActionLauncher", - args: ["{that}", "{arguments}.0", "{arguments}.1"] // value, oldValue + args: ["{that}"] }, openSearchKeyboard: { funcName: "gamepad.inputMapper.openSearchKeyboard", - args: ["{that}", "{arguments}.0", "{arguments}.1"] // value, oldValue + args: ["{that}"] }, openConfigPanel: { funcName: "gamepad.inputMapper.openConfigPanel", - args: ["{that}", "{arguments}.0", "{arguments}.1"] // value, oldValue + args: ["{that}"] } }, components: { @@ -202,17 +199,18 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE gamepad.inputMapper.updateFormFieldText = function (that) { if (that.model.lastExternalFocused && gamepad.inputMapperUtils.content.isTextInput(that.model.lastExternalFocused)) { that.model.lastExternalFocused.value = that.model.textInputValue; + + // TODO: Figure out a way to do this that doesn't require jQuery. + $(that.model.lastExternalFocused).trigger("change"); } }; gamepad.inputMapper.generateModalOpenFunction = function (modalKey) { - return function (that, value, oldValue) { - if (that.model.pageInView && value && !oldValue) { - // In this case we don't want to fail over to a modal's activeElement. - that.applier.change("lastExternalFocused", document.activeElement); + return function (that) { + // In this case we don't want to fail over to a modal's activeElement. + that.applier.change("lastExternalFocused", document.activeElement); - that.applier.change("activeModal", modalKey); - } + that.applier.change("activeModal", modalKey); }; }; @@ -220,49 +218,26 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE gamepad.inputMapper.openActionLauncher = gamepad.inputMapper.generateModalOpenFunction("actionLauncher"); gamepad.inputMapper.openSearchKeyboard = gamepad.inputMapper.generateModalOpenFunction("searchKeyboard"); - gamepad.inputMapper.openConfigPanel = function (that, value, oldValue) { - if (oldValue === 0 && value > that.model.prefs.analogCutoff) { - gamepad.inputMapperUtils.background.postMessage(that, { actionName: "openOptionsPage"}); - } + gamepad.inputMapper.openConfigPanel = function (that) { + gamepad.inputMapperUtils.background.postMessage(that, { action: "openOptionsPage"}); }; gamepad.inputMapper.handlePageInViewChange = function (that) { if (that.model.pageInView) { - gamepad.inputMapper.updateControls(that); + gamepad.inputMapper.loadSettings(that); } else { that.applier.change("activeModal", false); } }; - // TODO: Remove this once the new bindings are fully wired up. - /** - * - * (Re)load the gamepad configuration from local storage. - * - * @param {Object} that - The inputMapper component. - * - */ - gamepad.inputMapper.updateControls = function (that) { - if (that.model.pageInView) { - chrome.storage.local.get(["gamepadConfiguration"], function (configWrapper) { - var gamepadConfig = configWrapper.gamepadConfiguration; - - // Update the gamepad configuration only if it's available. - if (gamepadConfig) { - that.applier.change("map", gamepadConfig); - } - }); - } - }; - gamepad.inputMapper.loadSettings = async function (that) { gamepad.inputMapper.loadPrefs(that); gamepad.inputMapper.loadBindings(that); /* - The two params for the onChanged listener callbak are "changes" and "areaName". In our case, "areaName" is + The two params for the onChanged listener callback are "changes" and "areaName". In our case, "areaName" is always "local", so we ignore it. "changes" is an object with an entry for each changed key, as in: { "gamepad-prefs": newValue: {}} diff --git a/src/js/content_scripts/search-keyboard.js b/src/js/content_scripts/search-keyboard.js index cd6c0c9..7b3887a 100644 --- a/src/js/content_scripts/search-keyboard.js +++ b/src/js/content_scripts/search-keyboard.js @@ -69,7 +69,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE if (that.model.textInputValue && that.model.textInputValue.trim().length) { var actionOptions = { - actionName: "search", + action: "search", // See: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/search/query disposition: "NEW_TAB", text: that.model.textInputValue.trim() diff --git a/src/js/settings/addBinding.js b/src/js/settings/addBinding.js new file mode 100644 index 0000000..e1f1f3d --- /dev/null +++ b/src/js/settings/addBinding.js @@ -0,0 +1,128 @@ +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ +(function (fluid) { + "use strict"; + var gamepad = fluid.registerNamespace("gamepad"); + + fluid.defaults("gamepad.settings.ui.addBinding.selectWithNone", { + gradeNames: ["gamepad.ui.select"], + model: { + noneOption: true, + noneDescription: "Select an option" + } + }); + + fluid.defaults("gamepad.settings.ui.addBinding", { + gradeNames: ["gamepad.templateRenderer"], + injectionType: "replaceWith", + markup: { + container: "
\nWhen the
\nis pressed,\n
\n\n
" + }, + model: { + hidden: false, + + action: "none", + actionChoices: {}, + + index: "none", + availableIndexChoices: {}, + + missingParams: true + }, + modelListeners: { + hidden: { + this: "{that}.container", + method: "toggleClass", + args: ["hidden", "{change}.value"] + }, + availableIndexChoices: { + funcName: "gamepad.settings.ui.addBinding.checkIndexChoices", + args: ["{that}"] + }, + action: { + funcName: "gamepad.settings.ui.addBinding.checkForMissingParams", + args: ["{that}"] + }, + index: { + funcName: "gamepad.settings.ui.addBinding.checkForMissingParams", + args: ["{that}"] + } + }, + modelRelay: { + source: "{that}.model.missingParams", + target: "{that}.model.dom.addButton.attr.disabled" + }, + selectors: { + actionSelect: ".gamepad-settings-add-binding-action-select", + indexSelect: ".gamepad-settings-add-binding-index-select", + addButton: ".gamepad-settings-add-binding-addButton" + }, + // TODO: These throw errors about length, whether here or in the subcomponent definition in the parent. Fix. + invokers: { + handleClick: { + funcName: "gamepad.settings.ui.addBinding.notifyParent", + args: ["{that}", "{arguments}.0", "{gamepad.settings.ui.bindingsPanel}"] // event, parentComponent + } + }, + listeners: { + "onCreate.bindAddButton": { + this: "{that}.dom.addButton", + method: "click", + args: ["{that}.handleClick"] + } + }, + components: { + indexSelect: { + container: "{that}.dom.indexSelect", + type: "gamepad.settings.ui.addBinding.selectWithNone", + options: { + model: { + noneDescription: "- select an input -", + selectedChoice: "{gamepad.settings.ui.addBinding}.model.index", + // Unlike the bindings, we can use availableIndexChoices directly. + choices: "{gamepad.settings.ui.bindingsPanel}.model.availableIndexChoices" + } + } + }, + actionSelect: { + container: "{that}.dom.actionSelect", + type: "gamepad.settings.ui.addBinding.selectWithNone", + options: { + model: { + noneDescription: "- select an action -", + selectedChoice: "{gamepad.settings.ui.addBinding}.model.action", + choices: "{gamepad.settings.ui.addBinding}.model.actionChoices" + } + } + } + } + }); + + gamepad.settings.ui.addBinding.checkForMissingParams = function (that) { + var missingParams = !that.model.index || that.model.index === "none" || !that.model.action || that.model.action === "none"; + that.applier.change("missingParams", missingParams); + }; + + gamepad.settings.ui.addBinding.notifyParent = function (that, event, parentComponent) { + event.preventDefault(); + + parentComponent.addBinding(that.model); + + that.applier.change("action", false); + that.applier.change("index", false); + }; + + gamepad.settings.ui.addBinding.checkIndexChoices = function (that) { + var hasAvailableIndexChoices = typeof that.model.availableIndexChoices === "object" && Object.keys(that.model.availableIndexChoices).length > 0; + that.applier.change("hidden", !hasAvailableIndexChoices); + }; +})(fluid); diff --git a/src/js/settings/bindingsPanels.js b/src/js/settings/bindingsPanels.js new file mode 100644 index 0000000..1242ab9 --- /dev/null +++ b/src/js/settings/bindingsPanels.js @@ -0,0 +1,312 @@ +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ +(function (fluid) { + "use strict"; + var gamepad = fluid.registerNamespace("gamepad"); + + fluid.defaults("gamepad.settings.ui.bindingsPanel", { + gradeNames: ["gamepad.settings.ui.editableSection"], + markup: { + container: "

%label

" + }, + selectors: { + add: ".gamepad-settings-add-binding-container", + dynamic: ".dynamic-binding-components" + }, + model: { + label: "Bindings", + // The original bindings, in the canonical format. + bindings: {}, + // Our changes to all bindings, in the canonical format. + draftBindings: {}, + // A structure derived from draftBindings that is used to instantiate dynamic components. + bindingComponentSources: {}, + totalIndexes: 16, + + indexChoices: {}, + availableIndexChoices: {} + }, + modelListeners: { + bindings: [ + { + // excludeSource: "init", + funcName: "gamepad.settings.ui.bindingsPanel.resetDraft", + args: ["{that}"] + } + ], + draftBindings: [ + { + // excludeSource: "init", + funcName: "gamepad.settings.ui.bindingsPanel.flagDraftChanged", + args: ["{that}"] + }, + { + funcName: "gamepad.settings.ui.bindingsPanel.trackAvailableIndexChoices", + args: ["{that}"] + } + ], + "bindingComponentSources": { + excludeSource: ["init", "local"], + funcName: "gamepad.settings.ui.bindingsPanel.reconcileChildComponentChanges", + args: ["{that}", "{change}"] + } + }, + modelRelay: [ + { + source: "bindings", + target: "draftBindings", + backward: { + excludeSource: "*" + } + }, + { + source: "draftBindings", + target: "bindingComponentSources", + singleTransform: "gamepad.settings.ui.bindingsPanel.bindingsToComponentSources", + backward: { + excludeSource: "*" + } + } + ], + invokers: { + saveDraft: { + funcName: "gamepad.settings.ui.bindingsPanel.saveDraft", + args: ["{that}"] + }, + resetDraft: { + funcName: "gamepad.settings.ui.bindingsPanel.resetDraft", + args: ["{that}"] + }, + addBinding: { + funcName: "gamepad.settings.ui.bindingsPanel.addBinding", + args: ["{that}", "{arguments}.0"] // bindingComponentModel + }, + removeBinding: { + funcName: "gamepad.settings.ui.bindingsPanel.removeBinding", + args: ["{that}", "{arguments}.0"] // bindingComponentModel + } + }, + dynamicComponents: { + binding: { + container: "{that}.dom.dynamic", + sources: "{that}.model.bindingComponentSources", + type: "{source}.type", + options: { + model: { + index: "{source}.index", + + action: "{source}.action", + invert: "{source}.invert", + repeatRate: "{source}.repeatRate", + scrollFactor: "{source}.scrollFactor", + background: "{source}.background", + key: "{source}.key", + + actionChoices: "{gamepad.settings.ui.bindingsPanel}.options.actionChoices", + indexChoices: "{gamepad.settings.ui.bindingsPanel}.options.indexChoices", + availableIndexChoices: "{gamepad.settings.ui.bindingsPanel}.model.availableIndexChoices" + } + } + } + }, + components: { + addBindingPanel: { + container: "{that}.dom.add", + type: "gamepad.settings.ui.addBinding", + options: { + model: { + actionChoices: "{gamepad.settings.ui.bindingsPanel}.options.actionChoices", + + // indexChoices: "{gamepad.settings.ui.bindingsPanel}.options.indexChoices", + availableIndexChoices: "{gamepad.settings.ui.bindingsPanel}.model.availableIndexChoices" + } + } + } + } + }); + + /** + * + * Transform that.model.draftBindings to a map that includes the key we require, and which is keyed in a way + * that will support "lensing" UI components properly. + * + * @param {Object} originalBindings - The original bindings, keyed by axis/button index. + * @return {Object} - A restructured object whose entries include the original index, keyed by index and action. + * + */ + gamepad.settings.ui.bindingsPanel.bindingsToComponentSources = function (originalBindings) { + var componentSources = {}; + fluid.each(originalBindings, function (binding, index) { + var consolidatedBinding = fluid.copy(binding); + consolidatedBinding.index = index; + consolidatedBinding.type = "gamepad.settings.ui.editBinding." + consolidatedBinding.action; + + var combinedKey = index + "-" + binding.action; + componentSources[combinedKey] = consolidatedBinding; + }); + return componentSources; + }; + + gamepad.settings.ui.bindingsPanel.resetDraft = function (that) { + var transaction = that.applier.initiate(); + + transaction.fireChangeRequest({ path: "draftBindings", type: "DELETE"}); + transaction.fireChangeRequest({ path: "draftBindings", value: that.model.bindings }); + transaction.fireChangeRequest({ path: "draftClean", value: true}); + + transaction.commit(); + }; + + gamepad.settings.ui.bindingsPanel.flagDraftChanged = function (that) { + var draftClean = gamepad.utils.isDeeplyEqual(that.model.draftBindings, that.model.bindings); + that.applier.change("draftClean", draftClean); + }; + + gamepad.settings.ui.bindingsPanel.saveDraft = function (that) { + var transaction = that.applier.initiate(); + + transaction.fireChangeRequest({ path: "bindings", type: "DELETE"}); + transaction.fireChangeRequest({ path: "bindings", value: that.model.draftBindings }); + transaction.fireChangeRequest({ path: "draftClean", value: true}); + + transaction.commit(); + }; + + // Keep track of which buttons/axes are already bound so that we can update the drop-down menus. + gamepad.settings.ui.bindingsPanel.trackAvailableIndexChoices = function (that) { + var transaction = that.applier.initiate(); + + var availableIndexNumbers = []; + for (var bindingIndex = 0; bindingIndex < that.model.totalIndexes; bindingIndex++) { + if (that.model.draftBindings[bindingIndex] === undefined) { + // `toString` is required because filterKeys doesn't work with numeric keys. + availableIndexNumbers.push(bindingIndex.toString()); + } + } + + var availableIndexChoices = fluid.filterKeys(that.options.indexChoices, availableIndexNumbers); + + transaction.fireChangeRequest({ path: "availableIndexChoices", type: "DELETE"}); + transaction.fireChangeRequest({ path: "availableIndexChoices", value: availableIndexChoices}); + transaction.commit(); + }; + + /** + * + * The model from which this component was "sourced" has a totally different structure than the bindings we actually + * want to preserve. This function takes care of relaying changes back to the upstream structure, and in the + * original format. + * + * @param {Object} that - The binding panel component. + * @param {Object} change - See https://docs.fluidproject.org/infusion/development/changeapplierapi#the-special-context-change + * + */ + gamepad.settings.ui.bindingsPanel.reconcileChildComponentChanges = function (that, change) { + fluid.each(change.value, function (singleBinding, compositeKey) { + var transaction = that.applier.initiate(); + + var draftBinding = fluid.get(that, ["model", "bindingComponentSources", compositeKey]); + + // We can't continue without an action, as it would result in an unusable component type. + if (draftBinding.action) { + var oldBinding = fluid.get(change, ["oldValue", compositeKey]); + + // Remove the material unique to component sources, i.e. the index and type. + var filteredDraftBinding = fluid.filterKeys(draftBinding, ["action", "invert", "repeatRate", "scrollFactor", "background", "key"]); + var filteredOldBinding = fluid.filterKeys(oldBinding, ["action", "invert", "repeatRate", "scrollFactor", "background", "key"]); + + if (!gamepad.utils.isDeeplyEqual(filteredDraftBinding, filteredOldBinding)) { + transaction.fireChangeRequest({ path: ["draftBindings", draftBinding.index], type: "DELETE"}); + transaction.fireChangeRequest({ path: ["draftBindings", draftBinding.index], value: filteredDraftBinding }); + transaction.commit(); + } + } + }); + }; + + gamepad.settings.ui.bindingsPanel.addBinding = function (that, bindingComponentModel) { + var bindingIndex = fluid.get(bindingComponentModel, "index"); + if (bindingIndex === undefined) { + fluid.log(fluid.logLevel.ERROR, "Can't add binding because no index was provided."); + } + else if (that.model.draftBindings[bindingComponentModel.index]) { + fluid.log(fluid.logLevel.ERROR, "Can't add binding because the requested index is already in use."); + } + else { + var draftBindingContent = fluid.filterKeys(bindingComponentModel, ["action", "invert", "repeatRate", "scrollFactor", "background"]); + that.applier.change(["draftBindings", bindingIndex], draftBindingContent); + } + }; + + gamepad.settings.ui.bindingsPanel.removeBinding = function (that, bindingComponentModel) { + var transaction = that.applier.initiate(); + var newDraftBindings = fluid.copy(that.model.draftBindings); + delete newDraftBindings[bindingComponentModel.index]; + transaction.fireChangeRequest({ path: "draftBindings", type: "DELETE"}); + transaction.fireChangeRequest({ path: "draftBindings", value: newDraftBindings }); + transaction.commit(); + }; + + // Although there are joysticks with less buttons, the "standard" number of axes/buttons is described at: + // + // https://w3c.github.io/gamepad/#remapping + // + // Although this only describes 4 axes and 17 buttons, some controllers (Playstation, Switch) have an 18th button. + + // For future reference, on Windows there is a flag to enable support for the multitouch pad on PS4/5 controllers: + // + // chrome://flags/#enable-gamepad-multitouch + // + // We only support axes/buttons, but may add multitouch if this feature comes to other operating systems. + + fluid.defaults("gamepad.settings.ui.buttonsPanel", { + gradeNames: ["gamepad.settings.ui.bindingsPanel"], + actionChoices: gamepad.actions.choices.button, + indexChoices: { + "0": "A Button (Xbox), X Button (PS4/5), B Button (Switch)", + "1": "B Button (Xbox), Circle Button (PS4/5), A Button (Switch)", + "2": "X Button (Xbox), Square Button (PS4/5), Y (Switch)", + "3": "Y Button (Xbox), Triangle Button (PS4/5)), X (Switch)", + "4": "Left Bumper", + "5": "Right Bumper", + "6": "Left Trigger", + "7": "Right Trigger", + "8": "Back Button (Xbox), Share Button (PS4/5), Minus Button (Switch)", + "9": "Start Button (Xbox), Options Button (PS4/5), Plus Button (Switch)", + "10": "Left Thumbstick Button", + "11": "Right Thumbstick Button", + "12": "D-Pad, Up Button", + "13": "D-Pad, Down Button", + "14": "D-Pad, Left Button", + "15": "D-Pad, Right Button", + "16": "Badge Button (Xbox, PS4/5), Share Button (Switch)", + "17": "Touchpad Click (PS4/5), Home Button (Switch)" + }, + model: { + totalIndexes: 18 + } + }); + + fluid.defaults("gamepad.settings.ui.axesPanel", { + gradeNames: ["gamepad.settings.ui.bindingsPanel"], + actionChoices: gamepad.actions.choices.axis, + indexChoices: { + "0": "Left Stick, Horizontal Axis", + "1": "Left Stick, Vertical Axis", + "2": "Right Stick, Horizontal Axis", + "3": "Right Stick, Vertical Axis" + }, + model: { + totalIndexes: 4 + } + }); +})(fluid); diff --git a/src/js/settings/draftHandlingButton.js b/src/js/settings/draftHandlingButton.js new file mode 100644 index 0000000..acb34c5 --- /dev/null +++ b/src/js/settings/draftHandlingButton.js @@ -0,0 +1,42 @@ +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ +(function (fluid) { + "use strict"; + fluid.defaults("gamepad.settings.draftHandlingButton", { + gradeNames: ["gamepad.templateRenderer"], + markup: { + container: "" + }, + model: { + label: "Draft Button", + disabled: true + }, + modelRelay: { + source: "{that}.model.disabled", + target: "{that}.model.dom.container.attr.disabled" + } + }); + + fluid.defaults("gamepad.settings.draftHandlingButton.discard", { + gradeNames: ["gamepad.settings.draftHandlingButton"], + model: { + label: "Discard Changes" + } + }); + + fluid.defaults("gamepad.settings.draftHandlingButton.save", { + gradeNames: ["gamepad.settings.draftHandlingButton"], + model: { + label: "Save Changes" + } + }); +})(fluid); diff --git a/src/js/settings/editBinding.js b/src/js/settings/editBinding.js new file mode 100644 index 0000000..909b3cf --- /dev/null +++ b/src/js/settings/editBinding.js @@ -0,0 +1,524 @@ +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ +(function (fluid) { + "use strict"; + + var gamepad = fluid.registerNamespace("gamepad"); + + fluid.defaults("gamepad.settings.ui.editBinding.base", { + gradeNames: ["gamepad.templateRenderer"], + icon: gamepad.svg["options-icon"], + markup: { + container: "
" + }, + selectors: { + description: ".gamepad-settings-binding-description", + toggleParams: ".gamepad-settings-params-icon", + removeButton: ".gamepad-settings-binding-removeButton", + params: ".gamepad-settings-binding-params" + }, + model: { + hideParams: true + }, + modelListeners: { + hideParams: [ + { + this: "{that}.dom.params", + method: "toggleClass", + args: ["hidden", "{change}.value"] + } + ] + }, + invokers: { + removeBinding: { + func: "{gamepad.settings.ui.bindingsPanel}.removeBinding", + args: ["{that}.model"] // bindingComponentModel + } + }, + listeners: { + "onCreate.bindRemoveButton": { + this: "{that}.dom.removeButton", + method: "click", + args: ["{that}.removeBinding"] + }, + "onCreate.drawIcon": { + funcName: "gamepad.settings.ui.editBinding.base.drawIcon", + args: ["{that}"] + } + }, + components: { + description: { + container: "{that}.dom.description", + type: "gamepad.settings.ui.bindingDescription", + options: { + actionChoices: "{gamepad.settings.ui.bindingsPanel}.options.actionChoices", + indexChoices: "{gamepad.settings.ui.bindingsPanel}.options.indexChoices", + + model: { + action: "{gamepad.settings.ui.editBinding.base}.model.action", + index: "{gamepad.settings.ui.editBinding.base}.model.index" + } + } + } + } + }); + + gamepad.settings.ui.editBinding.base.drawIcon = function (that) { + var iconElement = that.locate("toggleParams"); + iconElement.html(that.options.icon); + }; + + // Our unique UI components (custom select, etc.) + + fluid.defaults("gamepad.settings.ui.bindingDescription", { + gradeNames: ["gamepad.templateRenderer"], + actionChoices: {}, + indexChoices: {}, + markup: { + container: "When the %indexDescription is pressed, %actionDescription." + }, + selectors: { + actionDescription: ".action-description", + indexDescription: ".index-description" + }, + model: { + action: false, + actionDescription: "", + index: false, + indexDescription: "" + }, + modelListeners: { + action: { + funcName: "gamepad.settings.ui.bindingDescription.choiceToDescription", + args: ["{that}", "{that}.options.actionChoices", "{that}.model.action", "actionDescription"] // choices, choiceIndex, descriptionKey + }, + index: { + funcName: "gamepad.settings.ui.bindingDescription.choiceToDescription", + args: ["{that}", "{that}.options.indexChoices", "{that}.model.index", "indexDescription"] // choices, choiceIndex, descriptionKey + } + }, + modelRelay: [ + { + source: "indexDescription", + target: "dom.indexDescription.text" + }, + { + source: "actionDescription", + target: "dom.actionDescription.text" + } + ] + }); + + gamepad.settings.ui.bindingDescription.choiceToDescription = function (that, choices, choiceIndex, descriptionKey) { + var choiceDescription = (choices && choices[choiceIndex]) || "???"; + that.applier.change(descriptionKey, choiceDescription); + }; + + fluid.defaults("gamepad.settings.ui.params.select", { + gradeNames: ["gamepad.ui.select"], + model: { + availableChoices: {} + }, + markup: { + container: "
\n
" + }, + model: { + label: "Key", + description: "The key to send.", + selectedChoice: "{gamepad.settings.ui.editBinding.base}.model.key", + choices: "{gamepad.settings.ui.editBinding.supportsKey}.options.keyChoices", + availableChoices: "{gamepad.settings.ui.editBinding.supportsKey}.options.keyChoices" + } + } + } + } + }); + // Here are the individual grade names directly correlated with action names. + + /* + + * Actions with no additional parameters: * + + || Action Key || Repeat Rate || Background || Invert || Scroll Factor || Key || + | ----------------------------- | ------------ | ----------- | ------- | -------------- | ---- | + | `click` | | | | | | + | `closeCurrentTab` | | | | | | + | `closeCurrentWindow` | | | | | | + | `maximizeWindow` | | | | | | + | `nextPageInHistory` | | | | | | + | `openActionLauncher` | | | | | | + | `openConfigPanel` | | | | | | + | `openSearchKeyboard` | | | | | | + | `previousPageInHistory` | | | | | | + | `restoreWindowSize` | | | | | | + | `reopenTabOrWindow` | | | | | | + + */ + + fluid.defaults("gamepad.settings.ui.editBinding.click", { gradeNames: ["gamepad.settings.ui.editBinding.base"] }); + fluid.defaults("gamepad.settings.ui.editBinding.closeCurrentTab", { gradeNames: ["gamepad.settings.ui.editBinding.base"] }); + fluid.defaults("gamepad.settings.ui.editBinding.closeCurrentWindow", { gradeNames: ["gamepad.settings.ui.editBinding.base"] }); + fluid.defaults("gamepad.settings.ui.editBinding.maximizeWindow", { gradeNames: ["gamepad.settings.ui.editBinding.base"] }); + fluid.defaults("gamepad.settings.ui.editBinding.nextPageInHistory", { gradeNames: ["gamepad.settings.ui.editBinding.base"] }); + fluid.defaults("gamepad.settings.ui.editBinding.openActionLauncher", { gradeNames: ["gamepad.settings.ui.editBinding.base"] }); + fluid.defaults("gamepad.settings.ui.editBinding.openConfigPanel", { gradeNames: ["gamepad.settings.ui.editBinding.base"] }); + fluid.defaults("gamepad.settings.ui.editBinding.openSearchKeyboard", { gradeNames: ["gamepad.settings.ui.editBinding.base"] }); + fluid.defaults("gamepad.settings.ui.editBinding.previousPageInHistory", { gradeNames: ["gamepad.settings.ui.editBinding.base"] }); + fluid.defaults("gamepad.settings.ui.editBinding.restoreWindowSize", { gradeNames: ["gamepad.settings.ui.editBinding.base"] }); + fluid.defaults("gamepad.settings.ui.editBinding.reopenTabOrWindow", { gradeNames: ["gamepad.settings.ui.editBinding.base"] }); + + /* + + * Actions that only support the `background` parameter: * + + || Action Key || Repeat Rate || Background || Invert || Scroll Factor || Key || + | ----------------------------- | ---------- | ----------- | ------- | -------------- | ---- | + | `openNewTab` | | Yes | | | | + | `openNewWindow` | | Yes | | | | + + */ + + fluid.defaults("gamepad.settings.ui.editBinding.openNewTab", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsBackground"], + model: gamepad.actions.button.openNewTab + }); + + fluid.defaults("gamepad.settings.ui.editBinding.openNewWindow", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsBackground"], + model: gamepad.actions.button.openNewWindow + }); + + /* + + * Actions that only support the `repeatRate` parameter: * + + || Action Key || Repeat Rate || Background || Invert || Scroll Factor || Key || + | ----------------------------- | ------------ | ----------- | ------- | -------------- | ---- | + | `goToNextTab` | Yes | | | | | + | `goToNextWindow` | Yes | | | | | + | `goToPreviousTab` | Yes | | | | | + | `goToPreviousWindow` | Yes | | | | | + | `tabForward` | Yes | | | | | + | `tabBackward` | Yes | | | | | + | `zoomIn` | Yes | | | | | + | `zoomOut` | Yes | | | | | + + */ + + fluid.defaults("gamepad.settings.ui.editBinding.tabForward", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsRepeatRate"], + model: gamepad.actions.button.tabForward + }); + + fluid.defaults("gamepad.settings.ui.editBinding.goToNextTab", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsRepeatRate"], + model: gamepad.actions.button.goToNextTab + }); + + fluid.defaults("gamepad.settings.ui.editBinding.goToNextWindow", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsRepeatRate"], + model: gamepad.actions.button.goToNextWindow + }); + + fluid.defaults("gamepad.settings.ui.editBinding.goToPreviousTab", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsRepeatRate"], + model: gamepad.actions.button.goToPreviousTab + }); + + fluid.defaults("gamepad.settings.ui.editBinding.goToPreviousWindow", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsRepeatRate"], + model: gamepad.actions.button.goToPreviousWindow + }); + + fluid.defaults("gamepad.settings.ui.editBinding.tabBackward", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsRepeatRate"], + model: gamepad.actions.button.tabBackward + }); + + fluid.defaults("gamepad.settings.ui.editBinding.zoomIn", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsRepeatRate"], + model: gamepad.actions.button.zoomIn + }); + + fluid.defaults("gamepad.settings.ui.editBinding.zoomOut", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsRepeatRate"], + model: gamepad.actions.button.zoomOut + }); + + + /* + + * `repeatRate` and `key` * + + || Action Key || Repeat Rate || Background || Invert || Scroll Factor || Key || + | ----------------------------- | ------------ | ----------- | ------- | -------------- | ---- | + | `sendKey` | Yes | | | | Yes | + + */ + + fluid.defaults("gamepad.settings.ui.editBinding.sendKey", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsRepeatRate", "gamepad.settings.ui.editBinding.supportsKey"], + model: gamepad.actions.button.sendKey + }); + + /* + + * `repeatRate` and `invert` * + + || Action Key || Repeat Rate || Background || Invert || Scroll Factor || Key || + | ----------------------------- | ------------ | ----------- | ------- | -------------- | ---- | + | `thumbstickHistoryNavigation` | Yes | | Yes | | | + | `thumbstickHorizontalArrows` | Yes | | Yes | | | + | `thumbstickTabbing` | Yes | | Yes | | | + | `thumbstickVerticalArrows` | Yes | | Yes | | | + | `thumbstickWindowSize` | Yes | | Yes | | | + | `thumbstickZoom` | Yes | | Yes | | | + + */ + + fluid.defaults("gamepad.settings.ui.editBinding.thumbstickHistoryNavigation", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsRepeatRate", "gamepad.settings.ui.editBinding.supportsInvert"], + model: gamepad.actions.axis.thumbstickHistoryNavigation + }); + + fluid.defaults("gamepad.settings.ui.editBinding.thumbstickHorizontalArrows", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsRepeatRate", "gamepad.settings.ui.editBinding.supportsInvert"], + model: gamepad.actions.axis.thumbstickHorizontalArrows + }); + + fluid.defaults("gamepad.settings.ui.editBinding.thumbstickTabbing", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsRepeatRate", "gamepad.settings.ui.editBinding.supportsInvert"], + model: gamepad.actions.axis.thumbstickTabbing + }); + + fluid.defaults("gamepad.settings.ui.editBinding.thumbstickVerticalArrows", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsRepeatRate", "gamepad.settings.ui.editBinding.supportsInvert"], + model: gamepad.actions.axis.thumbstickVerticalArrows + }); + + fluid.defaults("gamepad.settings.ui.editBinding.thumbstickWindowSize", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsRepeatRate", "gamepad.settings.ui.editBinding.supportsInvert"], + model: gamepad.actions.axis.thumbstickWindowSize + }); + + fluid.defaults("gamepad.settings.ui.editBinding.thumbstickZoom", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsRepeatRate", "gamepad.settings.ui.editBinding.supportsInvert"], + model: gamepad.actions.axis.thumbstickZoom + }); + + /* + * `repeatRate` and `scrollFactor` * + + || Action Key || Repeat Rate || Background || Invert || Scroll Factor || Key || + | ----------------------------- | ------------ | ----------- | ------- | -------------- | ---- | + | `scrollDown` | Yes | | | Yes | | + | `scrollLeft` | Yes | | | Yes | | + | `scrollRight` | Yes | | | Yes | | + | `scrollUp` | Yes | | | Yes | | + */ + + fluid.defaults("gamepad.settings.ui.editBinding.scrollDown", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsRepeatRate", "gamepad.settings.ui.editBinding.supportsScrollFactor"], + model: gamepad.actions.button.scrollDown + }); + + fluid.defaults("gamepad.settings.ui.editBinding.scrollLeft", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsRepeatRate", "gamepad.settings.ui.editBinding.supportsScrollFactor"], + model: gamepad.actions.button.scrollLeft + }); + + fluid.defaults("gamepad.settings.ui.editBinding.scrollRight", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsRepeatRate", "gamepad.settings.ui.editBinding.supportsScrollFactor"], + model: gamepad.actions.button.scrollRight + }); + + fluid.defaults("gamepad.settings.ui.editBinding.scrollUp", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsRepeatRate", "gamepad.settings.ui.editBinding.supportsScrollFactor"], + model: gamepad.actions.button.scrollUp + }); + + /* + + * `repeatRate`, `invert`, and `scrollFactor` * + + || Action Key || Repeat Rate || Background || Invert || Scroll Factor || Key || + | ----------------------------- | ------------ | ----------- | ------- | -------------- | ---- | + | `scrollHorizontally' | Yes | | Yes | Yes | | + | `scrollVertically` | Yes | | Yes | Yes | | + + */ + + fluid.defaults("gamepad.settings.ui.editBinding.scrollHorizontally", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsRepeatRate", "gamepad.settings.ui.editBinding.supportsInvert", "gamepad.settings.ui.editBinding.supportsScrollFactor"], + model: gamepad.actions.axis.scrollHorizontally + }); + + fluid.defaults("gamepad.settings.ui.editBinding.scrollVertically", { + gradeNames: ["gamepad.settings.ui.editBinding.hasParams", "gamepad.settings.ui.editBinding.supportsRepeatRate", "gamepad.settings.ui.editBinding.supportsInvert", "gamepad.settings.ui.editBinding.supportsScrollFactor"], + model: gamepad.actions.axis.scrollVertically + }); +})(fluid); diff --git a/src/js/settings/editableSection.js b/src/js/settings/editableSection.js new file mode 100644 index 0000000..c6e5837 --- /dev/null +++ b/src/js/settings/editableSection.js @@ -0,0 +1,72 @@ +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ +(function (fluid) { + "use strict"; + + fluid.defaults("gamepad.settings.ui.editableSection", { + gradeNames: ["gamepad.templateRenderer"], + markup: { + container: "

%label

" + }, + selectors: { + body: ".gamepad-settings-editable-section-body", + footer: ".gamepad-settings-editable-section-footer" + }, + model: { + label: "Editable Section", + classNames: "", + draftClean: true + }, + components: { + discardButton: { + container: "{that}.dom.footer", + type: "gamepad.settings.draftHandlingButton.discard", + options: { + model: { + disabled: "{gamepad.settings.ui.editableSection}.model.draftClean" + }, + listeners: { + "onCreate.bindClick": { + this: "{gamepad.settings.draftHandlingButton}.container", + method: "click", + args: ["{gamepad.settings.ui.editableSection}.resetDraft"] + } + } + } + }, + saveButton: { + container: "{that}.dom.footer", + type: "gamepad.settings.draftHandlingButton.save", + options: { + model: { + disabled: "{gamepad.settings.ui.editableSection}.model.draftClean" + }, + listeners: { + "onCreate.bindClick": { + this: "{gamepad.settings.draftHandlingButton}.container", + method: "click", + args: ["{gamepad.settings.ui.editableSection}.saveDraft"] + } + } + } + } + }, + invokers: { + saveDraft: { + funcName: "fluid.notImplemented" + }, + resetDraft: { + funcName: "fluid.notImplemented" + } + } + }); +})(fluid); diff --git a/src/js/settings/prefsPanel.js b/src/js/settings/prefsPanel.js new file mode 100644 index 0000000..b578ffe --- /dev/null +++ b/src/js/settings/prefsPanel.js @@ -0,0 +1,145 @@ +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ +(function (fluid) { + "use strict"; + var gamepad = fluid.registerNamespace("gamepad"); + + fluid.defaults("gamepad.settings.ui.prefsPanel", { + gradeNames: ["gamepad.settings.ui.editableSection"], + model: { + label: "General Preferences", + classNames: " gamepad-settings-prefs-panel", + prefs: {}, + draftPrefs: {} + }, + modelRelay: { + source: "prefs", + target: "draftPrefs", + backward: { + excludeSource: "*" + } + }, + modelListeners: { + prefs: { + excludeSource: "init", + funcName: "gamepad.settings.ui.prefsPanel.resetDraft", + args: ["{that}"] + + }, + draftPrefs: { + excludeSource: "local", + funcName: "gamepad.settings.ui.prefsPanel.flagDraftChanged", + args: ["{that}"] + } + }, + invokers: { + saveDraft: { + funcName: "gamepad.settings.ui.prefsPanel.saveDraft", + args: ["{that}"] + }, + resetDraft: { + funcName: "gamepad.settings.ui.prefsPanel.resetDraft", + args: ["{that}"] + } + }, + components: { + vibrate: { + container: "{that}.dom.body", + type: "gamepad.ui.toggle", + options: { + model: { + label: "Vibrate", + description: "Vibrate when an action cannot be completed.", + checked: "{gamepad.settings.ui.prefsPanel}.model.draftPrefs.vibrate" + } + } + }, + openWindowOnStartup: { + container: "{that}.dom.body", + type: "gamepad.ui.toggle", + options: { + model: { + label: "Open Settings on Startup", + description: "Open this configuration panel on startup if no other controllable windows are open.", + checked: "{gamepad.settings.ui.prefsPanel}.model.draftPrefs.openWindowOnStartup" + } + } + }, + analogCutoff: { + container: "{that}.dom.body", + type: "gamepad.ui.rangeInput", + options: { + model: { + label: "Analog Cutoff", + description: "Analog inputs below this value will be ignored. Useful to avoid problems with "jitter" on thumb sticks, or to avoid accidentally triggering inputs.", + + min: 0, + step: 0.05, + max: 0.95, + value: "{gamepad.settings.ui.prefsPanel}.model.draftPrefs.analogCutoff" + } + } + }, + newTabOrWindowURL: { + container: "{that}.dom.body", + type: "gamepad.ui.textInput", + options: { + model: { + label: "New Page/Tab URL", + description: "This URL will be used when opening new tabs or windows.", + value: "{gamepad.settings.ui.prefsPanel}.model.draftPrefs.newTabOrWindowURL" + } + } + }, + pollingFrequency: { + container: "{that}.dom.body", + type: "gamepad.ui.rangeInput", + options: { + model: { + label: "Polling Frequency", + description: "How often (in milliseconds) to check gamepads for input changes.", + + min: 10, + step: 10, + max: 250, + value: "{gamepad.settings.ui.prefsPanel}.model.draftPrefs.pollingFrequency" + } + } + } + } + }); + + gamepad.settings.ui.prefsPanel.resetDraft = function (that) { + var transaction = that.applier.initiate(); + + transaction.fireChangeRequest({ path: "draftPrefs", type: "DELETE"}); + transaction.fireChangeRequest({ path: "draftPrefs", value: that.model.prefs }); + transaction.fireChangeRequest({ path: "draftClean", value: true}); + + transaction.commit(); + }; + + gamepad.settings.ui.prefsPanel.flagDraftChanged = function (that) { + var draftClean = gamepad.utils.isDeeplyEqual(that.model.draftPrefs, that.model.prefs); + that.applier.change("draftClean", draftClean); + }; + + gamepad.settings.ui.prefsPanel.saveDraft = function (that) { + var transaction = that.applier.initiate(); + + transaction.fireChangeRequest({ path: "prefs", type: "DELETE"}); + transaction.fireChangeRequest({ path: "prefs", value: that.model.draftPrefs }); + transaction.fireChangeRequest({ path: "draftClean", value: true}); + + transaction.commit(); + }; +})(fluid); diff --git a/src/js/settings/rangeInput.js b/src/js/settings/rangeInput.js new file mode 100644 index 0000000..8fe6682 --- /dev/null +++ b/src/js/settings/rangeInput.js @@ -0,0 +1,155 @@ +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ +(function (fluid) { + "use strict"; + + var gamepad = fluid.registerNamespace("gamepad"); + + // A base grade without selectors or model relays, so that we can more easily change the behaviour in variants. + fluid.defaults("gamepad.ui.rangeInput", { + gradeNames: ["gamepad.templateRenderer"], + model: { + label: "Range Input", + value: 0, + summary: "" + }, + markup: { + container: "
\n
%label
\n
\n
%description
\n
\n
\n\n
\n
\n
%summary
\n
\n
" + }, + modelListeners: { + value: { + funcName: "gamepad.ui.rangeInput.createSummary", + args: ["{that}"] + } + }, + templates: { + summary: "%value", + noValue: "%value" + }, + selectors: { + input: ".gamepad-range-input", + + min: ".gamepad-range-min", + max: ".gamepad-range-max", + + summary: ".gamepad-range-summary" + }, + modelRelay: [ + // Input attributes + { + source: "{that}.model.min", + target: "dom.input.attr.min" + }, + { + source: "{that}.model.max", + target: "dom.input.attr.max" + }, + { + source: "{that}.model.step", + target: "dom.input.attr.step" + }, + { + source: "{that}.model.value", + target: "dom.input.value" + }, + + // Onscreen text + { + source: "{that}.model.min", + target: "dom.min.text" + }, + { + source: "{that}.model.max", + target: "dom.max.text" + }, + { + source: "{that}.model.summary", + target: "dom.summary.text" + } + ], + invokers: { + handleInputChange: { + funcName: "gamepad.ui.rangeInput.handleInputChange", + args: ["{that}", "{arguments}.0"] // event + }, + handleKeydown: { + funcName: "gamepad.ui.rangeInput.handleKeydown", + args: ["{that}", "{arguments}.0"] // event + } + }, + listeners: { + "onCreate.bindChange": { + this: "{that}.dom.input", + method: "change", + args: ["{that}.handleInputChange"] + }, + // Bind to the outer container to let the range input prevent default on keys it responds to, but handle + // ones it ignores. + "onCreate.bindKeydown": { + this: "{that}.container", + method: "keydown", + args: ["{that}.handleKeydown"] + } + } + }); + + gamepad.ui.rangeInput.createSummary = function (that) { + if (that.model.value) { + var summary = fluid.stringTemplate(that.options.templates.summary, that.model); + that.applier.change("summary", summary); + } + else { + that.applier.change("summary", that.options.templates.noValue); + } + }; + + gamepad.ui.rangeInput.handleInputChange = function (that, event) { + var newValue = event.target.value || 0; + try { + var numberValue = parseFloat(newValue); + that.applier.change("value", numberValue); + } + catch (error) { + fluid.log("Invalid range input: '" + newValue + "'."); + } + }; + + gamepad.ui.rangeInput.handleKeydown = function (that, event) { + var isTrusted = fluid.get(event, "originalEvent.isTrusted"); + if (!isTrusted) { + // Arrow navigation handling + if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].indexOf(event.code) !== -1) { + event.preventDefault(); + + var inputElement = that.locate("input"); + var inputDomElement = inputElement[0]; + + if (event.code === "ArrowLeft") { + inputDomElement.stepDown(); + } + else if (event.code === "ArrowRight") { + inputDomElement.stepUp(); + } + else if (event.code === "ArrowUp") { + inputDomElement.stepDown(); + } + else if (event.code === "ArrowDown") { + inputDomElement.stepUp(); + } + + // Simulate a change so that any differences we input will get picked up. + // TODO: Figure out how to do this without jQuery + inputElement.trigger("change"); + } + } + }; +})(fluid); diff --git a/src/js/settings/selectInput.js b/src/js/settings/selectInput.js new file mode 100644 index 0000000..fc2a4cf --- /dev/null +++ b/src/js/settings/selectInput.js @@ -0,0 +1,68 @@ +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ +(function (fluid) { + "use strict"; + + var gamepad = fluid.registerNamespace("gamepad"); + + // TODO: Ensure that gamepad "arrow" inputs and or "tabs" move up and down in the list. + + fluid.defaults("gamepad.ui.select", { + gradeNames: ["gamepad.templateRenderer"], + markup: { + container: "
\n\n
", + noneOption: "\n", + option: "\n" + }, + selectors: { + select: ".gamepad-select-input-input" + }, + model: { + noneOption: false, + noneDescription: "Select an option", + + selectedChoice: false, + // Should be a map of "value": "text description" + choices: { + } + }, + modelRelay: [ + { + source: "{that}.model.dom.select.value", + target: "{that}.model.selectedChoice" + } + ], + modelListeners: { + choices: { + funcName: "gamepad.ui.select.renderOptions", + args: ["{that}.dom.select", "{that}.options.markup.option", "{that}.model.choices", "{that}.model.selectedChoice", "{that}.model.noneOption", "{that}.options.markup.noneOption", "{that}.model.noneDescription"] // selectInputElement, optionTemplate, choices, selectedChoice, hasNoneOption, noneOptionTemplate, noneDescription + } + } + }); + + gamepad.ui.select.renderOptions = function (selectInputElement, optionTemplate, choices, selectedChoice, hasNoneOption, noneOptionTemplate, noneDescription) { + var renderedText = ""; + + if (hasNoneOption) { + var noneOptionText = fluid.stringTemplate(noneOptionTemplate, { noneDescription }); + renderedText += noneOptionText; + } + + fluid.each(choices, function (description, value) { + var selected = (value === selectedChoice) ? " selected" : ""; + var singleOptionText = fluid.stringTemplate(optionTemplate, { selected, description, value}); + renderedText += singleOptionText; + }); + + $(selectInputElement).html(renderedText); + }; +})(fluid); diff --git a/src/js/settings/settings.js b/src/js/settings/settings.js deleted file mode 100644 index a0e37e2..0000000 --- a/src/js/settings/settings.js +++ /dev/null @@ -1,441 +0,0 @@ -/* -Copyright (c) 2023 The Gamepad Navigator Authors -See the AUTHORS.md file at the top-level directory of this distribution and at -https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. - -Licensed under the BSD 3-Clause License. You may not use this file except in -compliance with this License. - -You may obtain a copy of the BSD 3-Clause License at -https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE -*/ -/* globals chrome */ -(function (fluid) { - "use strict"; - var gamepad = fluid.registerNamespace("gamepad"); - fluid.defaults("gamepad.settings", { - gradeNames: ["gamepad.templateRenderer"], - injectionType: "replaceWith", - markup: { - container: "
" - }, - model: { - prefs: gamepad.prefs.defaults, - bindings: gamepad.bindings.defaults - }, - modelListeners: { - prefs: { - excludeSource: "init", - funcName: "gamepad.settings.savePrefs", - args: ["{that}.model.prefs"] - }, - bindings: { - excludeSource: "init", - funcName: "gamepad.settings.saveBindings", - args: ["{that}.model.bindings"] - } - }, - listeners: { - "onCreate.loadSettings": { - funcName: "gamepad.settings.loadSettings", - args: ["{that}"] - } - }, - components: { - prefs: { - container: "{that}.container", - type: "gamepad.config.prefs", - options: { - model: { - prefs: "{gamepad.settings}.model.prefs" - } - } - } - // buttonBindings: { - // container: "{that}.dom.body", - // type: "gamepad.config.bindings", - // options: { - // model: { - // label: "Buttons / Triggers", - // bindings: "{gamepad.settings}.model.bindings.buttons" - // } - // } - // }, - // axisBindings: { - // container: "{that}.dom.body", - // type: "gamepad.config.bindings", - // options: { - // model: { - // label: "Axes (Thumb sticks)", - // bindings: "{gamepad.settings}.model.bindings.axes" - // } - // } - // } - } - }); - - gamepad.settings.loadSettings = async function (that) { - gamepad.settings.loadPrefs(that); - - gamepad.settings.loadBindings(that); - - // In the similar function in input mapper, we add a listener for changes to values in local storage. As we - // have code to ensure that there is only open settings panel, and since only the settings panel can update - // stored values, we should safely be able to avoid listening for local storage changes here. - }; - - gamepad.settings.loadPrefs = async function (that) { - var storedPrefs = await gamepad.utils.getStoredKey("gamepad-prefs"); - var prefsToSave = storedPrefs || gamepad.prefs.defaults; - - var transaction = that.applier.initiate(); - - transaction.fireChangeRequest({ path: "prefs", type: "DELETE"}); - transaction.fireChangeRequest({ path: "prefs", value: prefsToSave }); - - transaction.commit(); - }; - - gamepad.settings.loadBindings = async function (that) { - var storedBindings = await gamepad.utils.getStoredKey("gamepad-bindings"); - var bindingsToSave = storedBindings || gamepad.bindings.defaults; - - var transaction = that.applier.initiate(); - - transaction.fireChangeRequest({ path: "bindings", type: "DELETE"}); - transaction.fireChangeRequest({ path: "bindings", value: bindingsToSave }); - - transaction.commit(); - }; - - gamepad.settings.savePrefs = function (prefs) { - var prefsEqualDefaults = gamepad.utils.isDeeplyEqual(gamepad.prefs.defaults, prefs); - if (prefsEqualDefaults) { - chrome.storage.local.remove("gamepad-prefs"); - } - else { - chrome.storage.local.set({ "gamepad-prefs": prefs }); - } - }; - - gamepad.settings.saveBindings = async function (bindings) { - var bindingsEqualDefaults = gamepad.utils.isDeeplyEqual(gamepad.bindings.defaults, bindings); - if (bindingsEqualDefaults) { - chrome.storage.local.remove("gamepad-bindings"); - } - else { - chrome.storage.local.set({ "gamepad-bindings": bindings }); - } - }; - - fluid.defaults("gamepad.config.draftHandlingButton", { - gradeNames: ["gamepad.templateRenderer"], - markup: { - container: "" - }, - model: { - label: "Draft Button", - disabled: true - }, - modelRelay: { - source: "{that}.model.disabled", - target: "{that}.model.dom.container.attr.disabled" - } - }); - - fluid.defaults("gamepad.config.draftHandlingButton.discard", { - gradeNames: ["gamepad.config.draftHandlingButton"], - model: { - label: "Discard Changes" - } - }); - - fluid.defaults("gamepad.config.draftHandlingButton.save", { - gradeNames: ["gamepad.config.draftHandlingButton"], - model: { - label: "Save Changes" - } - }); - - - fluid.defaults("gamepad.config.editableSection", { - gradeNames: ["gamepad.templateRenderer"], - markup: { - container: "

%label

" - }, - selectors: { - body: ".gamepad-config-editable-section-body", - footer: ".gamepad-config-editable-section-footer" - }, - model: { - label: "Editable Section", - draftClean: true - }, - components: { - discardButton: { - container: "{that}.dom.footer", - type: "gamepad.config.draftHandlingButton.discard", - options: { - model: { - disabled: "{gamepad.config.editableSection}.model.draftClean" - }, - listeners: { - "onCreate.bindClick": { - this: "{gamepad.config.draftHandlingButton}.container", - method: "click", - args: ["{gamepad.config.editableSection}.resetDraft"] - } - } - } - }, - saveButton: { - container: "{that}.dom.footer", - type: "gamepad.config.draftHandlingButton.save", - options: { - model: { - disabled: "{gamepad.config.editableSection}.model.draftClean" - }, - listeners: { - "onCreate.bindClick": { - this: "{gamepad.config.draftHandlingButton}.container", - method: "click", - args: ["{gamepad.config.editableSection}.saveDraft"] - } - } - } - } - }, - invokers: { - saveDraft: { - funcName: "fluid.notImplemented" - }, - resetDraft: { - funcName: "fluid.notImplemented" - } - } - }); - - /* - analogCutoff: 0.25, // was 0.4 - - newTabOrWindowURL: "https://www.google.com/", - - openWindowOnStartup: true, - vibrate: true - */ - - fluid.defaults("gamepad.config.prefs", { - gradeNames: ["gamepad.config.editableSection"], - model: { - label: "Preferences", - prefs: {}, - draftPrefs: {} - }, - modelRelay: { - source: "prefs", - target: "draftPrefs", - backward: { - excludeSource: "*" - } - }, - modelListeners: { - prefs: { - excludeSource: "init", - funcName: "gamepad.config.prefs.resetDraft", - args: ["{that}"] - - }, - draftPrefs: { - excludeSource: "local", - funcName: "gamepad.config.prefs.flagDraftChanged", - args: ["{that}"] - } - }, - invokers: { - saveDraft: { - funcName: "gamepad.config.prefs.saveDraft", - args: ["{that}"] - }, - resetDraft: { - funcName: "gamepad.config.prefs.resetDraft", - args: ["{that}"] - } - }, - components: { - vibrate: { - container: "{that}.dom.body", - type: "gamepad.ui.toggle", - options: { - model: { - label: "Vibrate", - checked: "{gamepad.config.prefs}.model.draftPrefs.vibrate" - } - } - }, - openWindowOnStartup: { - container: "{that}.dom.body", - type: "gamepad.ui.toggle", - options: { - model: { - label: "Open Settings on Startup", - checked: "{gamepad.config.prefs}.model.draftPrefs.openWindowOnStartup" - } - } - } - } - }); - - gamepad.config.prefs.resetDraft = function (that) { - var transaction = that.applier.initiate(); - - transaction.fireChangeRequest({ path: "draftPrefs", type: "DELETE"}); - transaction.fireChangeRequest({ path: "draftPrefs", value: that.model.prefs }); - transaction.fireChangeRequest({ path: "draftClean", value: true}); - - transaction.commit(); - }; - - gamepad.config.prefs.flagDraftChanged = function (that) { - var draftClean = gamepad.utils.isDeeplyEqual(that.model.draftPrefs, that.model.prefs); - that.applier.change("draftClean", draftClean); - }; - - gamepad.config.prefs.saveDraft = function (that) { - var transaction = that.applier.initiate(); - - transaction.fireChangeRequest({ path: "prefs", type: "DELETE"}); - transaction.fireChangeRequest({ path: "prefs", value: that.model.draftPrefs }); - transaction.fireChangeRequest({ path: "draftClean", value: true}); - - transaction.commit(); - }; - - /* - Existing options we need to display/edit (and actions that use them`) - speedFactorOption: [ - "reverseTab", - "forwardTab", - "scrollLeft", - "scrollRight", - "scrollUp", - "scrollDown", - "scrollHorizontally", - "scrollVertically", - "thumbstickTabbing", - "thumbstickHorizontalArrows", - "thumbstickVerticalArrows" - ], - backgroundOption: ["openNewTab", "openNewWindow"], - invertOption: [ - "scrollHorizontally", - "scrollVertically", - "thumbstickHistoryNavigation", - "thumbstickTabbing", - "thumbstickZoom", - "thumbstickWindowSize", - "thumbstickHorizontalArrows", - "thumbstickVerticalArrows" - ] - - In addition, we need to make the existing hard-coded "frequency" option configurable for everything except: - - key: "click", - key: "openConfigPanel", - key: "openSearchKeyboard", - key: "openNewWindow", - key: "openNewTab", - key: "maximizeWindow", - key: "restoreWindowSize", - - // On the fence, but on balance I'd rather they not be repeatable. - key: "closeCurrentTab", - key: "closeCurrentWindow", - key: "reopenTabOrWindow", - key: "previousPageInHistory", - key: "nextPageInHistory", - - These should be repeatable: - - key: "goToPreviousWindow", - key: "goToNextWindow", - key: "goToPreviousTab", - key: "goToNextTab", - - key: "reverseTab", - key: "forwardTab", - key: "scrollLeft", - key: "scrollRight", - key: "scrollUp", - key: "scrollDown", - key: "zoomIn", - key: "zoomOut", - key: "sendArrowLeft", - key: "sendArrowRight", - key: "sendArrowUp", - key: "sendArrowDown", - - */ - - // Component to edit a section of the bindings, i.e. only axes or buttons. - fluid.defaults("gamepad.config.bindings", { - gradeNames: ["gamepad.config.editableSection"], - model: { - label: "Bindings", - bindings: {}, - draftBindings: "{that}.model.bindings" - }, - modelListeners: { - bindings: { - excludeSource: "init", - funcName: "gamepad.config.prefs.resetDraft", - args: ["{that}"] - - }, - draftBindings: { - excludeSource: "local", - funcName: "gamepad.config.prefs.flagDraftChanged", - args: ["{that}"] - } - }, - invokers: { - saveDraft: { - funcName: "gamepad.config.prefs.saveDraft", - args: ["{that}"] - }, - resetDraft: { - funcName: "gamepad.config.prefs.resetDraft", - args: ["{that}"] - } - } - }); - - gamepad.config.bindings.resetDraft = function (that) { - var transaction = that.applier.initiate(); - - transaction.fireChangeRequest({ path: "draftBindings", type: "DELETE"}); - transaction.fireChangeRequest({ path: "draftBindings", value: that.model.bindings }); - transaction.fireChangeRequest({ path: "draftClean", value: true}); - - transaction.commit(); - }; - - gamepad.config.bindings.flagDraftChanged = function (that) { - var draftClean = gamepad.utils.isDeeplyEqual(that.model.draftBindings, that.model.bindings); - that.applier.change("draftClean", draftClean); - }; - - gamepad.config.bindings.saveDraft = function (that) { - var transaction = that.applier.initiate(); - - transaction.fireChangeRequest({ path: "bindings", type: "DELETE"}); - transaction.fireChangeRequest({ path: "bindings", value: that.model.draftBindings }); - transaction.fireChangeRequest({ path: "draftClean", value: true}); - - transaction.commit(); - }; - - // TODO: As long as there are unbound buttons/axes, present an "add binding" - // form at the bottom of each list. - - gamepad.settings(".gamepad-settings-body"); -})(fluid); diff --git a/src/js/settings/settingsMainPanel.js b/src/js/settings/settingsMainPanel.js new file mode 100644 index 0000000..d1b5fe1 --- /dev/null +++ b/src/js/settings/settingsMainPanel.js @@ -0,0 +1,133 @@ +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ +/* globals chrome */ +(function (fluid) { + "use strict"; + var gamepad = fluid.registerNamespace("gamepad"); + fluid.defaults("gamepad.settings.ui.mainPanel", { + gradeNames: ["gamepad.templateRenderer"], + injectionType: "replaceWith", + markup: { + container: "
" + }, + model: { + prefs: gamepad.prefs.defaults, + bindings: gamepad.bindings.defaults + }, + modelListeners: { + prefs: { + excludeSource: "init", + funcName: "gamepad.settings.savePrefs", + args: ["{that}.model.prefs"] + }, + bindings: { + excludeSource: "init", + funcName: "gamepad.settings.saveBindings", + args: ["{that}.model.bindings"] + } + }, + listeners: { + "onCreate.loadSettings": { + funcName: "gamepad.settings.loadSettings", + args: ["{that}"] + } + }, + components: { + prefsPanel: { + container: "{that}.container", + type: "gamepad.settings.ui.prefsPanel", + options: { + model: { + prefs: "{gamepad.settings.ui.mainPanel}.model.prefs" + } + } + }, + + buttonsPanel: { + container: "{that}.container", + type: "gamepad.settings.ui.buttonsPanel", + options: { + model: { + label: "Buttons / Triggers", + bindings: "{gamepad.settings.ui.mainPanel}.model.bindings.buttons" + } + } + }, + axesPanel: { + container: "{that}.container", + type: "gamepad.settings.ui.axesPanel", + options: { + model: { + label: "Axes (Thumb sticks)", + bindings: "{gamepad.settings.ui.mainPanel}.model.bindings.axes" + } + } + } + } + }); + + gamepad.settings.loadSettings = async function (that) { + gamepad.settings.loadPrefs(that); + + gamepad.settings.loadBindings(that); + + // In the similar function in input mapper, we add a listener for changes to values in local storage. As we + // have code to ensure that there is only open settings panel, and since only the settings panel can update + // stored values, we should safely be able to avoid listening for local storage changes here. + }; + + gamepad.settings.loadPrefs = async function (that) { + var storedPrefs = await gamepad.utils.getStoredKey("gamepad-prefs"); + var prefsToSave = storedPrefs || gamepad.prefs.defaults; + + var transaction = that.applier.initiate(); + + transaction.fireChangeRequest({ path: "prefs", type: "DELETE"}); + transaction.fireChangeRequest({ path: "prefs", value: prefsToSave }); + + transaction.commit(); + }; + + gamepad.settings.loadBindings = async function (that) { + var storedBindings = await gamepad.utils.getStoredKey("gamepad-bindings"); + var bindingsToSave = storedBindings || gamepad.bindings.defaults; + + var transaction = that.applier.initiate(); + + transaction.fireChangeRequest({ path: "bindings", type: "DELETE"}); + transaction.fireChangeRequest({ path: "bindings", value: bindingsToSave }); + + transaction.commit(); + }; + + gamepad.settings.savePrefs = function (prefs) { + var prefsEqualDefaults = gamepad.utils.isDeeplyEqual(gamepad.prefs.defaults, prefs); + if (prefsEqualDefaults) { + chrome.storage.local.remove("gamepad-prefs"); + } + else { + chrome.storage.local.set({ "gamepad-prefs": prefs }); + } + }; + + gamepad.settings.saveBindings = async function (bindings) { + var bindingsEqualDefaults = gamepad.utils.isDeeplyEqual(gamepad.bindings.defaults, bindings); + if (bindingsEqualDefaults) { + chrome.storage.local.remove("gamepad-bindings"); + } + else { + chrome.storage.local.set({ "gamepad-bindings": bindings }); + } + }; + + window.component = gamepad.settings.ui.mainPanel(".gamepad-settings-body"); +})(fluid); diff --git a/src/js/settings/textInput.js b/src/js/settings/textInput.js new file mode 100644 index 0000000..85f6621 --- /dev/null +++ b/src/js/settings/textInput.js @@ -0,0 +1,53 @@ +/* +Copyright (c) 2023 The Gamepad Navigator Authors +See the AUTHORS.md file at the top-level directory of this distribution and at +https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. + +Licensed under the BSD 3-Clause License. You may not use this file except in +compliance with this License. + +You may obtain a copy of the BSD 3-Clause License at +https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE +*/ +(function (fluid) { + "use strict"; + + var gamepad = fluid.registerNamespace("gamepad"); + + fluid.defaults("gamepad.ui.textInput", { + gradeNames: ["gamepad.templateRenderer"], + model: { + label: "Text Input", + description: "Enter some text!", + value: 0 + }, + modelRelay: [{ + source: "{that}.model.value", + target: "dom.input.value" + }], + markup: { + container: "
%label
%description
" + }, + selectors: { + input: ".gamepad-text-input" + }, + invokers: { + handleInputChange: { + funcName: "gamepad.ui.textInput.handleInputChange", + args: ["{that}", "{arguments}.0"] + } + }, + listeners: { + "onCreate.bindChange": { + this: "{that}.dom.input", + method: "change", + args: ["{that}.handleInputChange"] + } + } + }); + + // TODO: This doesn't seem to work for the onscreen keyboard. + gamepad.ui.textInput.handleInputChange = function (that, event) { + that.applier.change("value", event.target.value); + }; +})(fluid); diff --git a/src/js/settings/toggle.js b/src/js/settings/toggle.js index b76caca..d6e60ee 100644 --- a/src/js/settings/toggle.js +++ b/src/js/settings/toggle.js @@ -23,7 +23,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE checked: true }, markup: { - container: "
%label
" + container: "
%label
%description
" }, selectors: { toggle: ".gamepad-toggle" diff --git a/src/js/shared/configuration-maps.js b/src/js/shared/configuration-maps.js deleted file mode 100644 index 2da88f6..0000000 --- a/src/js/shared/configuration-maps.js +++ /dev/null @@ -1,183 +0,0 @@ -/* -Copyright (c) 2023 The Gamepad Navigator Authors -See the AUTHORS.md file at the top-level directory of this distribution and at -https://github.com/fluid-lab/gamepad-navigator/raw/master/AUTHORS.md. - -Licensed under the BSD 3-Clause License. You may not use this file except in -compliance with this License. - -You may obtain a copy of the BSD 3-Clause License at -https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE -*/ - -(function (fluid) { - "use strict"; - - fluid.registerNamespace("gamepad.configMaps"); - - fluid.defaults("gamepad.configMaps", { - gradeNames: ["fluid.modelComponent"], - model: { - // TODO: Make functions used in axes to be reusable for analogue buttons. - map: { - buttons: { - // Face Button. - // Cross on PlayStation controller & A on Xbox controller. - "0": { - defaultAction: "click", - currentAction: null, - speedFactor: 1, - background: false - }, - // Face Button. - // Circle on PlayStation controller & B on Xbox controller. - "1": { - defaultAction: "openActionLauncher", - currentAction: null, - speedFactor: 1, - background: false - }, - // Face Button. - // Square on PlayStation controller & X on Xbox controller. - "2": { - defaultAction: "previousPageInHistory", - currentAction: null, - speedFactor: 1, - background: false - }, - // Face Button. - // Triangle on PlayStation controller & Y on Xbox controller. - "3": { - defaultAction: "nextPageInHistory", - currentAction: null, - speedFactor: 1, - background: false - }, - // Left Bumper. - "4": { - defaultAction: "reverseTab", - currentAction: null, - speedFactor: 2.5, - background: false - }, - // Right Bumper. - "5": { - defaultAction: "forwardTab", - currentAction: null, - speedFactor: 2.5, - background: false - }, - // Left Trigger. - "6": { - defaultAction: "scrollLeft", - currentAction: null, - speedFactor: 1, - background: false - }, - // Right Trigger. - "7": { - defaultAction: "scrollRight", - currentAction: null, - speedFactor: 1, - background: false - }, - // Select/Share on PlayStation controller & Back on Xbox controller. - "8": { - defaultAction: "closeCurrentTab", - currentAction: null, - speedFactor: 1, - background: false - }, - // Start/Options on PlayStation controller & Start on Xbox controller. - "9": { - defaultAction: "openNewTab", - currentAction: null, - speedFactor: 1, - background: false - }, - // Left thumbstick button. - "10": { - defaultAction: "closeCurrentWindow", - currentAction: null, - speedFactor: 1, - background: false - }, - // Right thumbstick button. - "11": { - defaultAction: "openNewWindow", - currentAction: null, - speedFactor: 1, - background: false - }, - // D-Pad up direction button. - "12": { - defaultAction: "goToPreviousWindow", - currentAction: null, - speedFactor: 1, - background: false - }, - // D-Pad down direction button. - "13": { - defaultAction: "goToNextWindow", - currentAction: null, - speedFactor: 1, - background: false - }, - // D-Pad left direction button. - "14": { - defaultAction: "goToPreviousTab", - currentAction: null, - speedFactor: 1, - background: false - }, - // D-Pad right direction button. - "15": { - defaultAction: "goToNextTab", - currentAction: null, - speedFactor: 1, - background: false - }, - // Badge icon - // PS button on PlayStation controller & Xbox logo button. - // Reserved for launching reconfiguration panel. - "16": null, - // Reserved for mousepad/touchpad functionality. - "17": null - }, - axes: { - // Left thumbstick horizontal axis. - "0": { - defaultAction: "scrollHorizontally", - currentAction: null, - speedFactor: 1, - invert: false - }, - // Left thumbstick vertical axis. - "1": { - defaultAction: "scrollVertically", - currentAction: null, - speedFactor: 1, - invert: false - }, - // Right thumbstick horizontal axis. - "2": { - defaultAction: "thumbstickHistoryNavigation", - currentAction: null, - speedFactor: 1, - invert: false - }, - // Right thumbstick vertical axis. - "3": { - defaultAction: "thumbstickTabbing", - currentAction: null, - speedFactor: 2.5, - invert: false - } - } - }, - commonConfiguration: { - homepageURL: "https://www.google.com/" - } - } - }); -})(fluid); diff --git a/src/js/shared/prefs.js b/src/js/shared/prefs.js index 58bbd1f..4c140e9 100644 --- a/src/js/shared/prefs.js +++ b/src/js/shared/prefs.js @@ -29,6 +29,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE analogCutoff: 0.25, newTabOrWindowURL: "https://www.google.com/", openWindowOnStartup: true, + pollingFrequency: 50, vibrate: true }; })(fluid); diff --git a/src/js/content_scripts/utils.js b/src/js/shared/utils.js similarity index 100% rename from src/js/content_scripts/utils.js rename to src/js/shared/utils.js diff --git a/src/manifest.json b/src/manifest.json index 649d790..f0d2f15 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -26,10 +26,10 @@ "js/lib/fluid-osk/keyboard.js", "js/lib/fluid-osk/keyboards.js", "js/lib/fluid-osk/inputs.js", - "js/content_scripts/utils.js", - "js/content_scripts/gamepad-navigator.js", - "js/shared/configuration-maps.js", "js/shared/prefs.js", + "js/shared/utils.js", + "js/content_scripts/gamepad-navigator.js", + "js/content_scripts/actions.js", "js/content_scripts/bindings.js", "js/content_scripts/styles.js", "js/content_scripts/svgs.js", diff --git a/tests/html/select-input.html b/tests/html/select-input.html new file mode 100644 index 0000000..de2b4fb --- /dev/null +++ b/tests/html/select-input.html @@ -0,0 +1,74 @@ + + + + Text Input + + + + + + + + + + + + +

A sample page to manually test support for select inputs.

+ +

Select Input, Nothing Selected

+ + + +

Select Input, Value Selected

+ + + +

Select Input, Disabled Value Selected

+ + + + +

Multi Select

+ + + +

Custom Select Input

+ +
+ + + + + diff --git a/tests/html/text-input.html b/tests/html/text-input.html index 2706223..827204d 100644 --- a/tests/html/text-input.html +++ b/tests/html/text-input.html @@ -4,7 +4,7 @@ Text Input -

A sample page to try the new onscreen keyboard with various text inputs.

+

A sample page to manually test the new onscreen keyboard with various text inputs.

Input with No Type