diff --git a/.eslintrc.json b/.eslintrc.json
index cec9abc..4f0c7af 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,10 +1,11 @@
{
"env": {
+ "es2017": true,
"browser": true
},
"extends": "eslint-config-fluid",
"rules": {
- "indent": [
+ "@stylistic/js/indent": [
"error",
4,
{ "SwitchCase": 1}
diff --git a/AUTHORS.md b/AUTHORS.md
index 88f9018..1f526ec 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -1,5 +1,5 @@
+
+
+
+ Gamepad Navigator Settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/images/gamepad-icon-128px.png b/src/images/gamepad-icon-128px.png
index 708d83c..b9bd877 100644
Binary files a/src/images/gamepad-icon-128px.png and b/src/images/gamepad-icon-128px.png differ
diff --git a/src/images/gamepad-icon-16px.png b/src/images/gamepad-icon-16px.png
index 9217588..0d9a47f 100644
Binary files a/src/images/gamepad-icon-16px.png and b/src/images/gamepad-icon-16px.png differ
diff --git a/src/images/gamepad-icon-32px.png b/src/images/gamepad-icon-32px.png
index 1f64b5b..9e13892 100644
Binary files a/src/images/gamepad-icon-32px.png and b/src/images/gamepad-icon-32px.png differ
diff --git a/src/images/gamepad-icon-48px.png b/src/images/gamepad-icon-48px.png
index 3e19627..461f08c 100644
Binary files a/src/images/gamepad-icon-48px.png and b/src/images/gamepad-icon-48px.png differ
diff --git a/src/images/gamepad-icon.png b/src/images/gamepad-icon.png
index dc23b60..cce10ad 100644
Binary files a/src/images/gamepad-icon.png and b/src/images/gamepad-icon.png differ
diff --git a/src/images/gamepad-icon.svg b/src/images/gamepad-icon.svg
new file mode 100644
index 0000000..5ce8c92
--- /dev/null
+++ b/src/images/gamepad-icon.svg
@@ -0,0 +1,28 @@
+
+
+
+
diff --git a/src/images/thumbstick.svg b/src/images/thumbstick.svg
new file mode 100644
index 0000000..719f9d6
--- /dev/null
+++ b/src/images/thumbstick.svg
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/js/background.js b/src/js/background.js
index 91cf67a..98a81db 100644
--- a/src/js/background.js
+++ b/src/js/background.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
@@ -33,38 +33,44 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
*
* Switch to the next or the previous tab in the current window.
*
- * @param {String} tabDirection - The direction in which the tab focus should change,
+ * @param {String} tabDirection - The direction in which the tab focus should change.
+ * @return {Promise} - A promise that will resolve with a response payload.
*
*/
- gamepad.messageListenerUtils.switchTab = function (tabDirection) {
- chrome.tabs.query({ currentWindow: true }, function (tabsArray) {
- // Switch only if more than one tab is present.
- if (tabsArray.length > 1) {
- // Find index of the currently active tab.
- var activeTabIndex = null;
- tabsArray.forEach(function (tab, index) {
- if (tab.active) {
- activeTabIndex = index;
- }
- });
+ gamepad.messageListenerUtils.switchTab = async function (tabDirection) {
+ var tabsArray = await chrome.tabs.query({ currentWindow: true });
+ // Filter to "controllable" tabs.
+ var filteredTabs = tabsArray.filter(gamepad.messageListenerUtils.filterControllableTabs);
- // Switch browser tab.
- if (tabDirection === "previousTab") {
- /**
- * If the first tab is focused then switch to the last tab.
- * Otherwise, switch to the previous tab.
- */
- if (activeTabIndex === 0) {
- activeTabIndex = tabsArray.length;
- }
- chrome.tabs.update(tabsArray[activeTabIndex - 1].id, { active: true });
+ // Switch only if more than one tab is present.
+ if (filteredTabs.length > 1) {
+ // Find index of the currently active tab.
+ var activeTabIndex = null;
+ filteredTabs.forEach(function (tab, index) {
+ if (tab.active) {
+ activeTabIndex = index;
}
- else if (tabDirection === "nextTab") {
- // Switch to the next tab.
- chrome.tabs.update(tabsArray[(activeTabIndex + 1) % tabsArray.length].id, { active: true });
+ });
+
+ // Switch browser tab.
+ if (tabDirection === "previousTab") {
+ /**
+ * If the first tab is focused then switch to the last tab.
+ * Otherwise, switch to the previous tab.
+ */
+ if (activeTabIndex === 0) {
+ activeTabIndex = filteredTabs.length;
}
+ await chrome.tabs.update(filteredTabs[activeTabIndex - 1].id, { active: true });
}
- });
+ else if (tabDirection === "nextTab") {
+ // Switch to the next tab.
+ await chrome.tabs.update(filteredTabs[(activeTabIndex + 1) % filteredTabs.length].id, { active: true });
+ }
+ }
+ else {
+ return { vibrate: true };
+ }
};
/**
@@ -90,12 +96,51 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
/**
*
* Close the currently opened window.
+ * @return {Promise} - A `Promise` that will complete with a response payload.
*
*/
- gamepad.messageListenerUtils.closeCurrentWindow = function () {
- chrome.windows.getCurrent(function (currentWindow) {
- chrome.windows.remove(currentWindow.id);
- });
+ gamepad.messageListenerUtils.closeCurrentWindow = async function () {
+ var windowArray = await chrome.windows.getAll({ populate: true});
+ var controllableWindows = windowArray.filter(gamepad.messageListenerUtils.filterControllableWindows);
+ if (controllableWindows.length > 1) {
+ var focusedWindow = false;
+ var focusedWindowIndex = -1;
+
+ controllableWindows.forEach(function (window, index) {
+ if (window.focused) {
+ focusedWindow = window;
+ focusedWindowIndex = index;
+ }
+ });
+
+ if (focusedWindow) {
+ var newFocusIndex = (focusedWindowIndex + 1) % controllableWindows.length;
+ var windowToFocus = controllableWindows[newFocusIndex];
+
+ chrome.windows.remove(focusedWindow.id);
+
+ chrome.windows.update(windowToFocus.id, {
+ focused: true
+ });
+ }
+ }
+ else {
+ return { vibrate: true };
+ }
+ };
+
+ // Exclude tabs whose URL begins with `chrome://`, which do not contain
+ // our scripts and cannot be controlled using a gamepad.
+ gamepad.messageListenerUtils.filterControllableTabs = function (tabElement) {
+ return tabElement.url && !tabElement.url.startsWith("chrome://");
+ };
+
+ // Exclude windows that do not contain controllable tabs (see above).
+ gamepad.messageListenerUtils.filterControllableWindows = function (windowElement) {
+ if (!windowElement.tabs) { return false; }
+
+ var filteredTabs = windowElement.tabs.filter(gamepad.messageListenerUtils.filterControllableTabs);
+ return filteredTabs.length > 0;
};
/**
@@ -105,54 +150,58 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
* @param {String} windowDirection - The direction in which the window focus should change,
*
*/
- gamepad.messageListenerUtils.switchWindow = function (windowDirection) {
- chrome.windows.getLastFocused(function (focusedWindow) {
- if (focusedWindow) {
- chrome.windows.getAll(function (windowsArray) {
- // Switch only if more than one window is present.
- if (windowsArray.length > 1) {
- // Find the index of the currently active window.
- var focusedWindowIndex = null;
- for (var index = 0; index < windowsArray.length; index++) {
- if (focusedWindowIndex === null) {
- var window = windowsArray[index];
- if (window.id === focusedWindow.id) {
- focusedWindowIndex = index;
- }
- }
+ gamepad.messageListenerUtils.switchWindow = async function (windowDirection) {
+ var focusedWindow = await chrome.windows.getLastFocused();
+ if (focusedWindow) {
+ var windowsArray = await chrome.windows.getAll({ populate: true});
+ // Filter to controllable windows.
+ var filteredWindows = windowsArray.filter(gamepad.messageListenerUtils.filterControllableWindows);
+
+ // Switch only if more than one window is present.
+ if (filteredWindows.length > 1) {
+ // Find the index of the currently active window.
+ var focusedWindowIndex = null;
+ for (var index = 0; index < filteredWindows.length; index++) {
+ if (focusedWindowIndex === null) {
+ var window = filteredWindows[index];
+ if (window.id === focusedWindow.id) {
+ focusedWindowIndex = index;
}
+ }
+ }
- if (focusedWindowIndex === null) {
- throw new Error("Can't detect focused browser window.");
+ if (focusedWindowIndex === null) {
+ throw new Error("Can't detect focused browser window.");
+ }
+ else {
+ var windowIndexToFocus = focusedWindowIndex;
+ // Switch browser window.
+ if (windowDirection === "previousWindow") {
+ if (focusedWindowIndex === 0) {
+ windowIndexToFocus = filteredWindows.length - 1;
}
else {
- var windowIndexToFocus = focusedWindowIndex;
- // Switch browser window.
- if (windowDirection === "previousWindow") {
- if (focusedWindowIndex === 0) {
- windowIndexToFocus = windowsArray.length - 1;
- }
- else {
- windowIndexToFocus = focusedWindowIndex - 1;
- }
- }
- else if (windowDirection === "nextWindow") {
- if (focusedWindowIndex >= windowsArray.length - 1) {
- windowIndexToFocus = 0;
- }
- else {
- windowIndexToFocus = focusedWindowIndex + 1;
- }
- }
-
- chrome.windows.update(windowsArray[windowIndexToFocus].id, {
- focused: true
- });
+ windowIndexToFocus = focusedWindowIndex - 1;
}
}
- });
+ else if (windowDirection === "nextWindow") {
+ if (focusedWindowIndex >= filteredWindows.length - 1) {
+ windowIndexToFocus = 0;
+ }
+ else {
+ windowIndexToFocus = focusedWindowIndex + 1;
+ }
+ }
+
+ await chrome.windows.update(filteredWindows[windowIndexToFocus].id, {
+ focused: true
+ });
+ }
}
- });
+ else {
+ return { vibrate: true };
+ }
+ }
};
/**
@@ -160,25 +209,30 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
* Change the zoom value of the current browser tab.
*
* @param {String} zoomType - Determines if the page should be zoomed in or out.
- *
+ * @return {Object} - A response payload if there are custom instructions for the client to follow up on.
*/
- gamepad.messageListenerUtils.setZoom = function (zoomType) {
- chrome.tabs.query({ currentWindow: true, active: true }, function (currentTab) {
- // Obtain the zoom value of the current tab.
- chrome.tabs.getZoom(currentTab.id, function (currentZoomFactor) {
- // Compute the new zoom value according to the zoom type.
- var newZoomFactor = null;
- if (zoomType === "zoomIn") {
- newZoomFactor = currentZoomFactor + 0.1;
- }
- else if (zoomType === "zoomOut") {
- newZoomFactor = currentZoomFactor - 0.1;
- }
+ gamepad.messageListenerUtils.setZoom = async function (zoomType) {
+ var currentTab = await chrome.tabs.query({ currentWindow: true, active: true });
- // Set the new zoom value.
- chrome.tabs.setZoom(currentTab.id, newZoomFactor);
- });
- });
+ // Obtain the zoom value of the current tab.
+ var currentZoomFactor = await chrome.tabs.getZoom(currentTab.id);
+
+ // Compute the new zoom value according to the zoom type.
+ var newZoomFactor = null;
+ if (zoomType === "zoomIn") {
+ newZoomFactor = currentZoomFactor + 0.1;
+ }
+ else if (zoomType === "zoomOut") {
+ newZoomFactor = currentZoomFactor - 0.1;
+ }
+
+ if (currentZoomFactor === newZoomFactor) {
+ return { vibrate: true };
+ }
+ else {
+ // Set the new zoom value.
+ chrome.tabs.setZoom(currentTab.id, newZoomFactor);
+ }
};
/**
@@ -189,147 +243,203 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
* @param {String} windowState - Value of the new state of the browser window.
* For example, "maximized", "minimized", etc.
* @param {Number} left - The position of the left edge of the screen (in pixels).
+ * @return {Promise} - A promise that will resolve with a response payload.
*
*/
- gamepad.messageListenerUtils.changeWindowSize = function (that, windowState, left) {
- chrome.windows.getCurrent(function (currentWindow) {
- chrome.windows.update(currentWindow.id, { state: windowState }, function () {
- /**
- * Set the dimensions of the window if the "maximized" state doesn't work
- * (Fallback Method).
- */
- chrome.windows.getCurrent(function (windowPostUpdate) {
- that.windowProperties[windowPostUpdate.id] = that.windowProperties[windowPostUpdate.id] || { isMaximized: null };
+ gamepad.messageListenerUtils.changeWindowSize = async function (that, windowState, left) {
+ var currentWindow = await chrome.windows.getCurrent();
- // Value of "state" on OS X is "fullscreen" if window is maximized.
- if (that.windowProperties[windowPostUpdate.id].isMaximized === null) {
- that.windowProperties[windowPostUpdate.id].isMaximized = windowPostUpdate.state === "fullscreen";
- }
- var isMaximized = that.windowProperties[windowPostUpdate.id].isMaximized;
-
- /**
- * Second check is for cases when the window is not maximized during
- * the first update.
- */
- if (windowState === "maximized" && windowPostUpdate.state !== "maximized" && !isMaximized) {
- // Preserve configuration before maximizing.
- that.windowProperties[windowPostUpdate.id] = {
- width: windowPostUpdate.width,
- height: windowPostUpdate.height,
- left: windowPostUpdate.left,
- top: windowPostUpdate.top
- };
-
- // Update window with the new properties.
- chrome.windows.update(windowPostUpdate.id, {
- width: screen.width,
- height: screen.height,
- left: left,
- top: 0
- });
-
- // Update the isMaximized member variable.
- that.windowProperties[windowPostUpdate.id].isMaximized = true;
- }
- else if (windowPostUpdate.state === "normal" && windowState === "normal" && isMaximized) {
- var previousProperties = that.windowProperties[windowPostUpdate.id];
-
- // Update window with the new properties.
- chrome.windows.update(windowPostUpdate.id, {
- width: previousProperties.width || Math.round(3 * screen.width / 5),
- height: previousProperties.height || Math.round(4 * screen.height / 5),
- left: previousProperties.left || (left + Math.round(screen.width / 15)),
- top: previousProperties.top || Math.round(screen.height / 15)
- });
-
- // Update the isMaximized member variable.
- that.windowProperties[windowPostUpdate.id].isMaximized = false;
- }
- });
+ if (currentWindow.state === windowState) {
+ return { vibrate: true };
+ }
+
+ await chrome.windows.update(currentWindow.id, { state: windowState });
+
+ var windowPostUpdate = await chrome.windows.getCurrent();
+ that.windowProperties[windowPostUpdate.id] = that.windowProperties[windowPostUpdate.id] || { isMaximized: null };
+
+ // Value of "state" on OS X is "fullscreen" if window is maximized.
+ if (that.windowProperties[windowPostUpdate.id].isMaximized === null) {
+ that.windowProperties[windowPostUpdate.id].isMaximized = windowPostUpdate.state === "fullscreen";
+ }
+ var isMaximized = that.windowProperties[windowPostUpdate.id].isMaximized;
+
+ /**
+ * Second check is for cases when the window is not maximized during
+ * the first update.
+ */
+ if (windowState === "maximized" && windowPostUpdate.state !== "maximized" && !isMaximized) {
+ // Preserve configuration before maximizing.
+ that.windowProperties[windowPostUpdate.id] = {
+ width: windowPostUpdate.width,
+ height: windowPostUpdate.height,
+ left: windowPostUpdate.left,
+ top: windowPostUpdate.top
+ };
+
+ // Update window with the new properties.
+ await chrome.windows.update(windowPostUpdate.id, {
+ width: screen.width,
+ height: screen.height,
+ left: left,
+ top: 0
});
+
+ // Update the isMaximized member variable.
+ that.windowProperties[windowPostUpdate.id].isMaximized = true;
+ }
+ else if (windowPostUpdate.state === "normal" && windowState === "normal" && isMaximized) {
+ var previousProperties = that.windowProperties[windowPostUpdate.id];
+
+ // Update window with the new properties.
+ await chrome.windows.update(windowPostUpdate.id, {
+ width: previousProperties.width || Math.round(3 * screen.width / 5),
+ height: previousProperties.height || Math.round(4 * screen.height / 5),
+ left: previousProperties.left || (left + Math.round(screen.width / 15)),
+ top: previousProperties.top || Math.round(screen.height / 15)
+ });
+
+ // Update the isMaximized member variable.
+ that.windowProperties[windowPostUpdate.id].isMaximized = false;
+ }
+ };
+
+ gamepad.messageListenerUtils.search = function (actionOptions) {
+ chrome.search.query({
+ disposition: actionOptions.disposition,
+ text: actionOptions.text
+ });
+ };
+
+ gamepad.messageListenerUtils.openOptionsPage = async function () {
+ var windowsArray = await chrome.windows.getAll({ populate: true});
+ var settingsWindowId = null;
+ var settingsTabId = null;
+ windowsArray.forEach(function (window) {
+ if (!settingsTabId) {
+ var settingsTab = window.tabs.find(function (tab) {
+ return tab.url.startsWith("chrome-extension://") && tab.url.endsWith("settings.html");
+ });
+ if (settingsTab) {
+ settingsWindowId = window.id;
+ settingsTabId = settingsTab.id;
+ }
+ }
});
+
+ // If there's already a settings tab, focus on it.
+ if (settingsTabId) {
+ await chrome.windows.update(settingsWindowId, { focused: true});
+ await chrome.tabs.update(settingsTabId, { active: true });
+ }
+ // Otherwise, open a new one.
+ else {
+ return await chrome.runtime.openOptionsPage();
+ }
};
var messageListener = {
// previous "members"
windowProperties: {},
- // previous "invokers"
- actionExecutor: function (actionData) {
- gamepad.messageListener.actionExecutor(messageListener, actionData);
+ // previous "invokers", or "actions".
+ // 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);
},
- // All actions are called with: tabId, invert, active, homepageURL, left
- openNewTab: function (tabId, invert, active, homepageURL) {
- // TODO: Currently opens a new tab and a new window.
- gamepad.messageListenerUtils.openNewTab(active, homepageURL);
+ closeCurrentTab: async function (actionOptions) {
+ if (actionOptions.tabId) {
+ var tabs = await chrome.tabs.query({currentWindow: true });
+ var controllableTabs = tabs.filter(gamepad.messageListenerUtils.filterControllableTabs);
+ // More than one controllable tab, just close the tab.
+ if (controllableTabs.length > 1) {
+ await chrome.tabs.remove(actionOptions.tabId);
+ }
+ // Fail over to the window close logic, which will check for controllable tabs in other windows.
+ else {
+ return await gamepad.messageListenerUtils.closeCurrentWindow();
+ }
+ }
},
- closeCurrentTab: function (tabId) {
- chrome.tabs.remove(tabId);
+ openNewWindow: async function (actionOptions) {
+ return await gamepad.messageListenerUtils.openNewWindow(actionOptions.active, actionOptions.homepageURL);
},
- goToPreviousTab: function () {
- gamepad.messageListenerUtils.switchTab("previousTab");
+
+ maximizeWindow: async function (actionOptions) {
+ return await gamepad.messageListenerUtils.changeWindowSize(messageListener, "maximized", actionOptions.left);
},
- goToNextTab: function () {
- gamepad.messageListenerUtils.switchTab("nextTab");
+ restoreWindowSize: async function (actionOptions) {
+ return await gamepad.messageListenerUtils.changeWindowSize(messageListener, "normal", actionOptions.left);
},
- openNewWindow: function (tabId, invert, active, homepageURL) {
- gamepad.messageListenerUtils.openNewWindow(active, homepageURL);
+
+ // Restored windows have focus, so there is still a danger of uncontrollable windows popping up.
+ reopenTabOrWindow: async function () {
+ await chrome.sessions.restore();
},
- closeCurrentWindow: gamepad.messageListenerUtils.closeCurrentWindow,
- goToPreviousWindow: function () {
- gamepad.messageListenerUtils.switchWindow("previousWindow");
+ goToPreviousWindow: async function () {
+ return await gamepad.messageListenerUtils.switchWindow("previousWindow");
},
- goToNextWindow: function () {
- gamepad.messageListenerUtils.switchWindow("nextWindow");
+ goToNextWindow: async function () {
+ return await gamepad.messageListenerUtils.switchWindow("nextWindow");
},
- zoomIn: function () {
- gamepad.messageListenerUtils.setZoom("zoomIn");
+ zoomIn: async function () {
+ return await gamepad.messageListenerUtils.setZoom("zoomIn");
},
- zoomOut: function () {
- gamepad.messageListenerUtils.setZoom("zoomOut");
+ zoomOut: async function () {
+ return await gamepad.messageListenerUtils.setZoom("zoomOut");
},
- maximizeWindow: function (tabId, invert, active, homepageURL, left) {
- gamepad.messageListenerUtils.changeWindowSize(messageListener, "maximized", left);
+ goToPreviousTab: async function () {
+ return await gamepad.messageListenerUtils.switchTab("previousTab");
},
- restoreWindowSize: function (tabId, invert, active, homepageURL, left) {
- gamepad.messageListenerUtils.changeWindowSize(messageListener, "normal", left);
+ goToNextTab: async function () {
+ return await gamepad.messageListenerUtils.switchTab("nextTab");
},
- reopenTabOrWindow: function () {
- chrome.sessions.restore();
+ closeCurrentWindow: gamepad.messageListenerUtils.closeCurrentWindow,
+ openActionLauncher: async function () {
+ return await gamepad.messageListenerUtils.openActionLauncher();
},
- openActionLauncher: function () {
- gamepad.messageListenerUtils.openActionLauncher();
- }
+ search: gamepad.messageListenerUtils.search,
+ openOptionsPage: gamepad.messageListenerUtils.openOptionsPage
};
- /**
- *
- * Calls the invoker methods according to the message is recieved from the content
- * script.
- *
- * @param {Object} messageListener - The messageListener.
- * @param {Object} actionData - The message object recieved from the content scripts.
- *
- */
- gamepad.messageListener.actionExecutor = function (messageListener, actionData) {
- // Execute the actions only if the action data is available.
- if (actionData.actionName) {
- var action = messageListener[actionData.actionName];
-
- // Trigger the action only if a valid action is found.
- if (action) {
- var invert = actionData.invert,
- active = actionData.active,
- left = actionData.left,
- homepageURL = actionData.homepageURL;
- chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
+ 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];
+
+ // Trigger the action only if a valid action is found.
+ if (action) {
+ var tabs = await chrome.tabs.query({ active: true, currentWindow: true });
var tabId = tabs[0] ? tabs[0].id : undefined;
- action(tabId, invert, active, homepageURL, left);
- });
+
+ var wrappedActionOptions = JSON.parse(JSON.stringify(actionOptions));
+ wrappedActionOptions.tabId = tabId;
+
+ var actionResult = await action(wrappedActionOptions);
+ port.postMessage(actionResult);
+ port.disconnect();
+ }
}
- }
- };
+ });
+ });
+
+ // If the user's preferences allow us to, open the settings panel on startup.
+ chrome.runtime.onStartup.addListener(function () {
+ chrome.storage.local.get(["gamepad-prefs"], function (storedObject) {
+ var storedPrefs = storedObject && storedObject["gamepad-prefs"];
+ // If our prefs exactly match the defaults, nothing is stored.
+ // In this case, the default behaviour is to open a window, so if
+ // there are no stored settings, we do that.
+ if (!storedPrefs || storedPrefs["openWindowOnStartup"]) {
+ chrome.runtime.openOptionsPage();
+ }
+ });
+ });
- // Instantiate and add action handler as a listener.
- chrome.runtime.onMessage.addListener(messageListener.actionExecutor);
+ // Open the settings panel when the icon is clicked.
+ chrome.action.onClicked.addListener(() => {
+ chrome.runtime.openOptionsPage();
+ });
})();
diff --git a/src/js/configuration_panel/button-listeners.js b/src/js/configuration_panel/button-listeners.js
index a44ed4c..1df3c7d 100644
--- a/src/js/configuration_panel/button-listeners.js
+++ b/src/js/configuration_panel/button-listeners.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
diff --git a/src/js/configuration_panel/configuration-panel.js b/src/js/configuration_panel/configuration-panel.js
index 8008e0d..675b230 100644
--- a/src/js/configuration_panel/configuration-panel.js
+++ b/src/js/configuration_panel/configuration-panel.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
@@ -99,7 +99,8 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
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"
+ openActionLauncher: "Open Action Launcher",
+ openSearchKeyboard: "Open Search"
},
axes: {
null: "None",
diff --git a/src/js/configuration_panel/create-panel-utils.js b/src/js/configuration_panel/create-panel-utils.js
index 2ca34af..181c264 100644
--- a/src/js/configuration_panel/create-panel-utils.js
+++ b/src/js/configuration_panel/create-panel-utils.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
diff --git a/src/js/configuration_panel/panel-events.js b/src/js/configuration_panel/panel-events.js
index e475ccf..121b97a 100644
--- a/src/js/configuration_panel/panel-events.js
+++ b/src/js/configuration_panel/panel-events.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
diff --git a/src/js/content_scripts/action-launcher.js b/src/js/content_scripts/action-launcher.js
index 438d8a9..ae02879 100644
--- a/src/js/content_scripts/action-launcher.js
+++ b/src/js/content_scripts/action-launcher.js
@@ -18,6 +18,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
fluid.defaults("gamepad.actionLauncher", {
gradeNames: ["gamepad.modal"],
model: {
+ classNames: " actionLauncher-modal",
label: "Gamepad Navigator: Launch Action"
},
components: {
@@ -36,13 +37,18 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
},
model: {
+ row: -1,
value: 1,
oldValue: 0,
- speedFactor: 1,
- invert: false,
- background: false,
- homepageURL: "https://www.google.com/",
- frequency: 100
+ commonConfiguration: {
+ homepageURL: "https://www.google.com/"
+ },
+ actionOptions: {
+ speedFactor: 1,
+ invert: false,
+ background: false,
+ frequency: 100
+ }
},
markup: {
@@ -57,6 +63,10 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
handleKeydown: {
funcName: "gamepad.actionLauncher.action.handleKeydown",
args: ["{that}", "{arguments}.0"] // event
+ },
+ handleFocus: {
+ funcName: "gamepad.actionLauncher.action.handleFocus",
+ args: ["{that}", "{arguments}.0"] // event
}
},
@@ -70,10 +80,32 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
this: "{that}.container",
method: "keydown",
args: ["{that}.handleKeydown"]
+ },
+ "onCreate.bindFocus": {
+ this: "{that}.container",
+ method: "focus",
+ args: ["{that}.handleFocus"]
+ }
+ },
+
+ modelListeners: {
+ "focusedRow": {
+ funcName: "gamepad.actionLauncher.action.handleFocusedRowChange",
+ args: ["{that}", "{that}.model.focusedRow", "{that}.model.row"]
}
}
});
+ gamepad.actionLauncher.action.handleFocusedRowChange = function (that, focusedRow, row) {
+ if (row === focusedRow) {
+ that.container[0].focus();
+ }
+ };
+
+ gamepad.actionLauncher.action.handleFocus = function (that) {
+ that.applier.change("focusedRow", that.model.row);
+ };
+
gamepad.actionLauncher.action.handleClick = function (actionComponent, inputMapperComponent, modalComponent, event) {
event.preventDefault();
@@ -86,27 +118,21 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
// triggered appropriately.
// All actions are called with:
- // value, speedFactor, invert, background, oldValue, homepageURL
+ // value, oldValue, actionOptions
// Simulate button down
actionFn(
1,
- actionComponent.model.speedFactor,
- actionComponent.model.invert,
- actionComponent.model.background,
0,
- actionComponent.model.homepageURL
+ actionComponent.model.actionOptions
);
// Simulate button up after a delay (100ms by default)
setTimeout(function () {
actionFn(
0,
- actionComponent.model.speedFactor,
- actionComponent.model.invert,
- actionComponent.model.background,
1,
- actionComponent.model.homepageURL
+ actionComponent.model.actionOptions
);
}, actionComponent.model.frequency);
}
@@ -126,93 +152,113 @@ 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.
- click: {
- description: "Click"
- },
- previousPageInHistory: {
- description: "History back button"
- },
- nextPageInHistory: {
- description: "History next button"
- },
- reverseTab: {
- description: "Focus on the previous element"
+ actionDefs: [
+ // All button-driven actions, except for the action launcher itself, ordered by subjective "usefulness".
+ {
+ key: "openConfigPanel",
+ description: "Configure Gamepad Navigator"
},
- forwardTab: {
- description: "Focus on the next element"
+ {
+ key: "openSearchKeyboard",
+ description: "Search"
},
- scrollLeft: {
- description: "Scroll left",
- frequency: 250
+ {
+ key: "openNewWindow",
+ description: "Open a new browser window"
},
- scrollRight: {
- description: "Scroll right",
- frequency: 250
+ {
+ key: "openNewTab",
+ description: "Open a new tab"
},
- scrollUp: {
- description: "Scroll up",
- frequency: 250
+ {
+ key: "goToPreviousWindow",
+ description: "Switch to the previous browser window"
},
- scrollDown: {
- description: "Scroll down",
- frequency: 250
+ {
+ key: "goToNextWindow",
+ description: "Switch to the next browser window"
},
- goToPreviousTab: {
+ {
+ key: "goToPreviousTab",
description: "Switch to the previous browser tab"
},
- goToNextTab: {
+ {
+ key: "goToNextTab",
description: "Switch to the next browser tab"
},
- closeCurrentTab: {
+ {
+ key: "closeCurrentTab",
description: "Close current browser tab"
},
- openNewTab: {
- description: "Open a new tab"
- },
- closeCurrentWindow: {
+ {
+ key: "closeCurrentWindow",
description: "Close current browser window"
},
- openNewWindow: {
- description: "Open a new browser window"
- },
- goToPreviousWindow: {
- description: "Switch to the previous browser window"
- },
- goToNextWindow: {
- description: "Switch to the next browser window"
+ {
+ key: "reopenTabOrWindow",
+ description: "Re-open the last closed tab or window"
},
- zoomIn: {
- description: "Zoom-in on the active web page"
+ {
+ key: "previousPageInHistory",
+ description: "History back button"
},
- zoomOut: {
- description: "Zoom-out on the active web page"
+ {
+ key: "nextPageInHistory",
+ description: "History next button"
},
- maximizeWindow: {
+ {
+ key: "maximizeWindow",
description: "Maximize the current browser window"
},
- restoreWindowSize: {
+ {
+ key: "restoreWindowSize",
description: "Restore the size of current browser window"
},
- reopenTabOrWindow: {
- description: "Re-open the last closed tab or window"
+ // These should nearly always already be bound.
+ {
+ key: "click",
+ description: "Click"
},
- sendArrowLeft: {
- description: "Send left arrow to the focused element."
+ {
+ key: "reverseTab",
+ description: "Focus on the previous element"
+ },
+ {
+ key: "forwardTab",
+ description: "Focus on the next element"
},
- sendArrowRight: {
- description: "Send right arrow to the focused element."
+ // Here for completeness, but IMO less likely to be used.
+ {
+ key: "scrollLeft",
+ description: "Scroll left",
+ frequency: 250
},
- sendArrowUp: {
- description: "Send up arrow to the focused element."
+ {
+ 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"
},
- sendArrowDown: {
- description: "Send down arrow to the focused element."
+ {
+ key: "zoomOut",
+ description: "Zoom-out on the active web page"
}
- // TODO: Add action to open configuration menu, when available.
- }
+ ]
},
dynamicComponents: {
@@ -222,15 +268,53 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
sources: "{that}.model.actionDefs",
options: {
model: {
- actionKey: "{sourcePath}",
+ focusedRow: "{gamepad.actionLauncher.actionsPanel}.model.focusedRow",
+ row: "{sourcePath}",
+ actionKey: "{source}.key",
description: "{source}.description",
- speedFactor: "{source}.speedFactor",
- invert: "{source}.invert",
- background: "{source}.background",
- frequency: "{source}.frequency"
+ actionOptions: {
+ speedFactor: "{source}.speedFactor",
+ invert: "{source}.invert",
+ background: "{source}.background",
+ frequency: "{source}.frequency"
+ }
}
}
}
+ },
+
+ invokers: {
+ handleKeydown: {
+ funcName: "gamepad.actionLauncher.actionsPanel.handleKeydown",
+ args: ["{that}", "{arguments}.0"] // event
+ }
+ },
+
+ listeners: {
+ "onCreate.bindKeydown": {
+ this: "{that}.container",
+ method: "keydown",
+ args: ["{that}.handleKeydown"]
+ }
}
});
+
+ gamepad.actionLauncher.actionsPanel.handleKeydown = function (that, event) {
+ if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.code)) {
+ event.preventDefault();
+ var lastRow = that.model.actionDefs.length - 1;
+ switch (event.code) {
+ case "ArrowLeft":
+ case "ArrowUp":
+ var previousRow = that.model.focusedRow > 0 ? that.model.focusedRow - 1 : lastRow;
+ that.applier.change("focusedRow", previousRow);
+ break;
+ case "ArrowRight":
+ case "ArrowDown":
+ var nextRow = that.model.focusedRow < lastRow ? that.model.focusedRow + 1 : 0;
+ that.applier.change("focusedRow", nextRow);
+ break;
+ }
+ }
+ };
})(fluid);
diff --git a/src/js/content_scripts/bindings.js b/src/js/content_scripts/bindings.js
new file mode 100644
index 0000000..25827f8
--- /dev/null
+++ b/src/js/content_scripts/bindings.js
@@ -0,0 +1,132 @@
+/*
+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
+*/
+
+/*
+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.bindings");
+
+ /*
+
+ "Action options", which represent the action to be performed in bindings and in internal calls between functions.
+
+ @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 {Boolean} [background] - For new windows/tabs, whether to open in the background.
+ @property {String} [key] - For `sendKey`, the key to send.
+
+ */
+
+ gamepad.bindings.defaults = {
+ buttons: {
+ // Cross on PS controller, A on Xbox
+ 0: {
+ action: "click"
+ },
+
+ // Circle on PS controller, B on Xbox.
+ // TODO: Make this work only in modals.
+ 1: {
+ action: "sendKey",
+ key: "Escape"
+ },
+
+ // Left Bumper.
+ "4": {
+ action: "reverseTab",
+ repeat: 1,
+ speed: 2.5
+ },
+ // Right Bumper.
+ "5": {
+ action: "forwardTab",
+ repeat: 1,
+ speed: 2.5
+ },
+
+ // Select button.
+ 8: {
+ action: "openSearchKeyboard"
+ },
+
+ // Start button.
+ 9: {
+ action: "openActionLauncher"
+ },
+
+ // D-pad.
+ // Up.
+ 12: {
+ action: "sendKey",
+ key: "ArrowUp",
+ repeat: 1,
+ speed: 1
+ },
+ // Down
+ 13: {
+ action: "sendKey",
+ key: "ArrowDown",
+ repeat: 1,
+ speed: 1
+ },
+ // Left
+ 14: {
+ action: "sendKey",
+ key: "ArrowLeft",
+ repeat: 1,
+ speedFactor: 1
+ },
+ // Right.
+ 15: {
+ action: "sendKey",
+ key: "ArrowRight",
+ repeat: 1,
+ speed: 1
+ },
+
+ // "Badge" button.
+ 16: {
+ action: "openConfigPanel"
+ }
+ },
+ 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,
+ invert: false
+ }
+ }
+ };
+})(fluid);
diff --git a/src/js/content_scripts/gamepad-navigator.js b/src/js/content_scripts/gamepad-navigator.js
index 4059b71..8c02565 100644
--- a/src/js/content_scripts/gamepad-navigator.js
+++ b/src/js/content_scripts/gamepad-navigator.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
@@ -19,6 +19,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
fluid.defaults("gamepad.navigator", {
gradeNames: ["fluid.modelComponent"],
model: {
+ // TODO: Figure out how this is used and how it differs from "in view";
connected: false,
axes: {},
buttons: {}
diff --git a/src/js/content_scripts/input-mapper-background-utils.js b/src/js/content_scripts/input-mapper-background-utils.js
index 13c5a7d..21d8d88 100644
--- a/src/js/content_scripts/input-mapper-background-utils.js
+++ b/src/js/content_scripts/input-mapper-background-utils.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
@@ -25,39 +25,40 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
* 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);
+ }
+ };
+
/**
*
- * Sends message to the background script to perform the given action.
+ * Connect to the background script, send a message, and handle the response.
*
* @param {Object} that - The inputMapper component.
- * @param {String} actionName - The action to be performed.
- * @param {Integer} value - The value of the gamepad input.
- * @param {Integer} oldValue - The previous value of the gamepad input.
- * @param {Boolean} background - Whether the new tab should open in background.
- * @param {String} homepageURL - The URL for the new tab.
+ * @param {String} actionOptions - The action payload to be transmitted.
*
*/
- gamepad.inputMapperUtils.background.sendMessage = function (that, actionName, value, oldValue, background, homepageURL) {
- if (value < oldValue && oldValue > that.options.cutoffValue) {
- // Set the data object for the action.
- var actionData = { actionName: actionName };
-
- // Set active key if background parameter is passed as an argument.
- if (background !== undefined) {
- actionData.active = !background;
- }
- if (homepageURL) {
- actionData.homepageURL = homepageURL;
+ gamepad.inputMapperUtils.background.postMessage = async function (that, actionOptions) {
+ // We use this because chrome.runtime.sendMessage did not leave enough time to receive a response.
+ var port = chrome.runtime.connect();
+ port.onMessage.addListener(function (response) {
+ var vibrate = fluid.get(response, "vibrate");
+ if (vibrate) {
+ that.vibrate();
}
+ });
- // Set the left pixel if the action is about changing "window size".
- if (actionName === "maximizeWindow" || actionName === "restoreWindowSize") {
- actionData.left = screen.availLeft;
- }
+ var wrappedActionOptions = fluid.copy(actionOptions);
+
+ wrappedActionOptions.homepageURL = that.model.commonConfiguration.homepageURL;
- // Send the message to the background script with the action details.
- chrome.runtime.sendMessage(actionData);
+ // Set the left pixel if the action is about changing "window size".
+ if (actionOptions.actionName === "maximizeWindow" || actionOptions.actionName === "restoreWindowSize") {
+ wrappedActionOptions.left = screen.availLeft;
}
+
+ port.postMessage(wrappedActionOptions);
};
/**
@@ -70,19 +71,21 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
* @param {Boolean} invert - Whether the zooming should be in opposite order.
*
*/
- gamepad.inputMapperUtils.background.thumbstickZoom = function (that, value, invert) {
+ gamepad.inputMapperUtils.background.thumbstickZoom = async function (that, value, invert) {
+ clearInterval(that.intervalRecords.zoomIn);
+ clearInterval(that.intervalRecords.zoomOut);
+
// Get the updated input value according to the configuration.
var inversionFactor = invert ? -1 : 1;
- value = value * inversionFactor;
- var zoomType = value > 0 ? "zoomOut" : "zoomIn",
- actionData = { actionName: zoomType };
- value = value * (value > 0 ? 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.
- clearInterval(that.intervalRecords.zoomIn);
- clearInterval(that.intervalRecords.zoomOut);
- if (value > that.options.cutoffValue) {
- that.intervalRecords[zoomType] = setInterval(chrome.runtime.sendMessage, that.options.frequency, actionData);
+ if (Math.abs(value) > that.options.cutoffValue) {
+ that.intervalRecords[zoomType] = setInterval(function (actionOptions) {
+ gamepad.inputMapperUtils.background.postMessage(that, actionOptions);
+ }, that.options.frequency, actionOptions);
}
};
@@ -98,23 +101,19 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
*/
gamepad.inputMapperUtils.background.thumbstickWindowSize = function (that, value, invert) {
// Get the updated input value according to the configuration.
- var inversionFactor = invert ? -1 : 1,
- windowSizeActionLabel = null;
- value = value * inversionFactor;
- if (value > 0) {
- windowSizeActionLabel = "maximizeWindow";
- }
- else {
- windowSizeActionLabel = "restoreWindowSize";
- value = value * -1;
- }
+ var inversionFactor = invert ? -1 : 1;
+ var polarisedValue = value * inversionFactor;
+
+ var actionName = polarisedValue > 0 ? "maximizeWindow" : "restoreWindowSize";
+
+ var actionOptions = {
+ actionName: actionName,
+ left: screen.availLeft
+ };
// Call the window size changing invokers according to the input value.
- if (value > that.options.cutoffValue) {
- chrome.runtime.sendMessage({
- actionName: windowSizeActionLabel,
- left: screen.availLeft
- });
+ if (Math.abs(value) > that.options.cutoffValue) {
+ gamepad.inputMapperUtils.background.postMessage(that, actionOptions);
}
};
})(fluid);
diff --git a/src/js/content_scripts/input-mapper-base.js b/src/js/content_scripts/input-mapper-base.js
index 39bd52d..26f09ce 100644
--- a/src/js/content_scripts/input-mapper-base.js
+++ b/src/js/content_scripts/input-mapper-base.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
@@ -16,12 +16,16 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
"use strict";
var gamepad = fluid.registerNamespace("gamepad");
- fluid.registerNamespace("gamepad.configMaps");
- fluid.registerNamespace("gamepad.inputMapper.base");
- fluid.registerNamespace("gamepad.inputMapperUtils.content");
+ // TODO: Fairly sure none of these are required.
+ // fluid.registerNamespace("gamepad.configMaps");
+ // fluid.registerNamespace("gamepad.inputMapper.base");
+ // fluid.registerNamespace("gamepad.inputMapperUtils.content");
fluid.defaults("gamepad.inputMapper.base", {
gradeNames: ["gamepad.configMaps", "gamepad.navigator"],
+ model: {
+ pageInView: true
+ },
modelListeners: {
"axes.*": {
funcName: "{that}.produceNavigation",
@@ -47,6 +51,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
* 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,
@@ -63,13 +68,21 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
},
currentTabIndex: 0,
tabbableElements: null,
- mutationObserverInstance: null
+ mutationObserverInstance: null,
+ // TODO: Ensure that there are sensible defaults somewhere.
+ prefs: {},
+ bindings: {}
},
// TODO: Make this configurable.
// "Jitter" cutoff Value for analog thumb sticks.
- cutoffValue: 0.40,
- scrollInputMultiplier: 50,
+ cutoffValue: 0.40, // TODO: Make this a preference
+ scrollInputMultiplier: 50, // TODO: Make this a preference
+
invokers: {
+ updateTabbables: {
+ funcName: "gamepad.inputMapper.base.updateTabbables",
+ args: ["{that}"]
+ },
produceNavigation: {
funcName: "gamepad.inputMapper.base.produceNavigation",
args: ["{that}", "{arguments}.0"]
@@ -86,19 +99,15 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
"this": "{that}.mutationObserverInstance",
method: "disconnect"
},
- // TODO: Investigate, identify, and fix tab navigation issues.
- tabindexSortFilter: {
- funcName: "gamepad.inputMapper.base.tabindexSortFilter",
- args: ["{arguments}.0", "{arguments}.1"]
+ vibrate: {
+ funcName: "gamepad.inputMapper.base.vibrate",
+ args: ["{that}"]
},
- /**
- * TODO: Add tests for links and other elements that involve navigation
- * between pages.
- */
- // Actions, these are called with: value, speedFactor, invert, background, oldValue, homepageURL
+
+ // Actions are called with value, oldValue, actionOptions
click: {
funcName: "gamepad.inputMapperUtils.content.click",
- args: ["{arguments}.0"]
+ args: ["{that}", "{arguments}.0"]
},
previousPageInHistory: {
funcName: "gamepad.inputMapperUtils.content.previousPageInHistory",
@@ -108,6 +117,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
funcName: "gamepad.inputMapperUtils.content.nextPageInHistory",
args: ["{that}", "{arguments}.0"]
},
+
reverseTab: {
funcName: "gamepad.inputMapperUtils.content.buttonTabNavigation",
args: ["{that}", "{arguments}.0", "reverseTab"]
@@ -116,21 +126,22 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
funcName: "gamepad.inputMapperUtils.content.buttonTabNavigation",
args: ["{that}", "{arguments}.0", "forwardTab"]
},
+
scrollLeft: {
funcName: "gamepad.inputMapperUtils.content.scrollLeft",
- args: ["{that}", "{arguments}.0", "{arguments}.1"]
+ args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"]
},
scrollRight: {
funcName: "gamepad.inputMapperUtils.content.scrollRight",
- args: ["{that}", "{arguments}.0", "{arguments}.1"]
+ args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"]
},
scrollUp: {
funcName: "gamepad.inputMapperUtils.content.scrollUp",
- args: ["{that}", "{arguments}.0", "{arguments}.1"]
+ args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"]
},
scrollDown: {
funcName: "gamepad.inputMapperUtils.content.scrollDown",
- args: ["{that}", "{arguments}.0", "{arguments}.1"]
+ args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"]
},
scrollHorizontally: {
funcName: "gamepad.inputMapperUtils.content.scrollHorizontally",
@@ -147,33 +158,40 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
// TODO: Add tests for when the number of tabbable elements changes.
thumbstickTabbing: {
funcName: "gamepad.inputMapperUtils.content.thumbstickTabbing",
- args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"]
+ args: ["{that}", "{arguments}.0", "{arguments}.2"]
+ },
+
+ sendKey: {
+ funcName: "gamepad.inputMapperUtils.content.sendKey",
+ args: ["{that}", "{arguments}.0", "{arguments}.2"] // value, 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: ["{arguments}.0", "ArrowLeft"] // value, key
+ args: ["{that}", "{arguments}.0", { key: "ArrowLeft" }] // value, actionOptions
},
sendArrowRight: {
funcName: "gamepad.inputMapperUtils.content.sendKey",
- args: ["{arguments}.0", "ArrowRight"] // value, key
+ args: ["{that}", "{arguments}.0", { key: "ArrowRight" }] // value, actionOptions
},
sendArrowUp: {
funcName: "gamepad.inputMapperUtils.content.sendKey",
- args: ["{arguments}.0", "ArrowUp"] // value, key
+ args: ["{that}", "{arguments}.0", { key: "ArrowUp" }] // value, actionOptions
},
sendArrowDown: {
funcName: "gamepad.inputMapperUtils.content.sendKey",
- args: ["{arguments}.0", "ArrowDown"] // value, key
+ args: ["{that}", "{arguments}.0", { key: "ArrowDown" }] // value, actionOptions
},
// Arrow actions for axes
thumbstickHorizontalArrows: {
funcName: "gamepad.inputMapperUtils.content.thumbstickArrows",
- args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "ArrowRight", "ArrowLeft"] // value, speedFactor, invert, forwardKey, backwardKey
+ args: ["{that}", "{arguments}.0", "{arguments}.2", "ArrowRight", "ArrowLeft"] // value, actionOptions, forwardKey, backwardKey
},
thumbstickVerticalArrows: {
funcName: "gamepad.inputMapperUtils.content.thumbstickArrows",
- args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2", "ArrowDown", "ArrowUp"] // value, speedFactor, invert, forwardKey, backwardKey
+ args: ["{that}", "{arguments}.0", "{arguments}.2", "ArrowDown", "ArrowUp"] // value, actionOptions, forwardKey, backwardKey
}
}
});
@@ -189,46 +207,50 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
* configured action map to produce a navigation effect.
*
* @param {Object} that - The inputMapper component.
- * @param {Object} change - The recipt for the change in input values.
+ * @param {Object} change - The receipt for the change in input values.
*
*/
gamepad.inputMapper.base.produceNavigation = function (that, change) {
- /**
- * Check if input is generated by axis or button and which button/axes was
- * disturbed.
- */
- var inputType = change.path[0],
- index = change.path[1],
- inputValue = change.value,
- oldInputValue = change.oldValue || 0;
+ // 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;
- var inputProperties = that.model.map[inputType][index],
- actionLabel = fluid.get(inputProperties, "currentAction") || fluid.get(inputProperties, "defaultAction"),
- homepageURL = that.model.commonConfiguration.homepageURL;
+ // 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
+ */
- /**
- * 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);
- // 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;
- // Trigger the action only if a valid function is found.
- if (action) {
- action(
- inputValue,
- inputProperties.speedFactor,
- inputProperties.invert,
- inputProperties.background,
- oldInputValue,
- homepageURL
- );
+ action(
+ inputValue,
+ oldInputValue,
+ actionOptions
+ );
+ }
}
}
};
@@ -247,6 +269,11 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
});
};
+ gamepad.inputMapper.base.updateTabbables = function (that) {
+ that.tabbableElements = ally.query.tabsequence({ strategy: "strict" });
+ };
+
+
/**
*
* A listener to track DOM elements and update the list when the DOM is updated.
@@ -259,13 +286,10 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
MutationObserver = that.options.windowObject.MutationObserver;
// Record the tabbable elements when the component is created.
- that.tabbableElements = ally.query.tabbable({ strategy: "strict" }).sort(that.tabindexSortFilter);
+ that.updateTabbables();
// Create an instance of the mutation observer.
- that.mutationObserverInstance = new MutationObserver(function () {
- // Record the tabbable elements when the DOM mutates.
- that.tabbableElements = ally.query.tabbable({ strategy: "strict" }).sort(that.tabindexSortFilter);
- });
+ that.mutationObserverInstance = new MutationObserver(that.updateTabbables);
// Specify the mutations to be observed.
var observerConfiguration = {
@@ -281,37 +305,25 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
that.mutationObserverInstance.observe(body, observerConfiguration);
};
- /**
- *
- * Filter for sorting the elements; to be used inside JavaScript's sort() method.
- *
- * @param {Object} elementOne - The DOM element.
- * @param {Object} elementTwo - The DOM element.
- * @return {Integer} - The value which will decide the order of the two elements.
- *
- */
- gamepad.inputMapper.base.tabindexSortFilter = function (elementOne, elementTwo) {
- var tabindexOne = parseInt(elementOne.getAttribute("tabindex")),
- tabindexTwo = parseInt(elementTwo.getAttribute("tabindex"));
- /**
- * If both elements have tabindex greater than 0, arrange them in ascending order
- * of the tabindex. Otherwise if only one of the elements have tabindex greater
- * than 0, place it before the other element. And in case, no element has a
- * tabindex attribute or both of them posses tabindex value equal to 0, keep them
- * in the same order.
- */
- if (tabindexOne > 0 && tabindexTwo > 0) {
- return tabindexOne - tabindexTwo;
- }
- else if (tabindexOne > 0) {
- return -1;
- }
- else if (tabindexTwo > 0) {
- return 1;
- }
- else {
- return 0;
+ gamepad.inputMapper.base.vibrate = function (that) {
+ if (that.model.prefs.vibrate) {
+ var gamepads = that.options.windowObject.navigator.getGamepads();
+ var nonNullGamepads = gamepads.filter(function (gamepad) { return gamepad !== null; });
+
+ fluid.each(nonNullGamepads, function (gamepad) {
+ if (gamepad.vibrationActuator) {
+ gamepad.vibrationActuator.playEffect(
+ "dual-rumble",
+ {
+ duration: 250,
+ startDelay: 0,
+ strongMagnitude: 0.25,
+ weakMagnitude: .75
+ }
+ );
+ }
+ });
}
};
})(fluid);
diff --git a/src/js/content_scripts/input-mapper-content-utils.js b/src/js/content_scripts/input-mapper-content-utils.js
index 94bc08e..6c8a05d 100644
--- a/src/js/content_scripts/input-mapper-content-utils.js
+++ b/src/js/content_scripts/input-mapper-content-utils.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
@@ -12,13 +12,11 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
/* global gamepad, ally, chrome */
-(function (fluid, $) {
+(function (fluid) {
"use strict";
fluid.registerNamespace("gamepad.inputMapperUtils.content");
- // TODO: Fix argument type in JSDoc comments, especially "value" and "speedFactor".
-
/**
* TODO: Fix the "speedFactor" usage in invokers to reduce the given interval loop
* frequency.
@@ -29,26 +27,28 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
* Scroll horizontally across the webpage.
*
* @param {Object} that - The inputMapper component.
- * @param {Integer} value - The value of the gamepad input (in pixels).
- * @param {Integer} speedFactor - Times by which the scroll speed should be increased.
- * @param {Boolean} invert - Whether the scroll should be in opposite order.
+ * @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).
*
*/
- gamepad.inputMapperUtils.content.scrollHorizontally = function (that, value, speedFactor, invert) {
- // Get the updated input value according to the configuration.
- var inversionFactor = invert ? -1 : 1;
- value = value * inversionFactor;
- if (value > 0) {
- clearInterval(that.intervalRecords.leftScroll);
- that.scrollRight(value, speedFactor);
- }
- else if (value < 0) {
- clearInterval(that.intervalRecords.rightScroll);
- that.scrollLeft(-1 * value, speedFactor);
- }
- else {
- clearInterval(that.intervalRecords.leftScroll);
- clearInterval(that.intervalRecords.rightScroll);
+ 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);
+ }
}
};
@@ -57,11 +57,14 @@ 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 value of the gamepad input (in pixels).
- * @param {Integer} speedFactor - Times by which the scroll speed should be increased.
+ * @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).
*
*/
- gamepad.inputMapperUtils.content.scrollLeft = function (that, value, speedFactor) {
+ 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.
@@ -72,11 +75,17 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
* Scroll the webpage towards the left only if the input value is more than the
* cutoff value.
*/
- if (value > that.options.cutoffValue) {
+ if (that.model.pageInView && (value > that.options.cutoffValue)) {
// Scroll to the left according to the new input value.
that.intervalRecords.leftScroll = setInterval(function () {
- var xOffset = $(that.options.windowObject).scrollLeft();
- $(that.options.windowObject).scrollLeft(xOffset - value * that.options.scrollInputMultiplier * speedFactor);
+ if (window.scrollX > 0) {
+ window.scroll(window.scrollX - value * that.options.scrollInputMultiplier * speedFactor, window.scrollY);
+ }
+ else {
+ clearInterval(that.intervalRecords.leftScroll);
+ that.vibrate();
+ }
+
}, that.options.frequency);
}
};
@@ -86,11 +95,14 @@ 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 value of the gamepad input (in pixels).
- * @param {Integer} speedFactor - Times by which the scroll speed should be increased.
+ * @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).
*
*/
- gamepad.inputMapperUtils.content.scrollRight = function (that, value, speedFactor) {
+ 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.
@@ -101,11 +113,17 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
* Scroll the webpage towards the right only if the input value is more than the
* cutoff value.
*/
- if (value > that.options.cutoffValue) {
+ if (that.model.pageInView && (value > that.options.cutoffValue)) {
// Scroll to the right according to the new input value.
that.intervalRecords.rightScroll = setInterval(function () {
- var xOffset = $(that.options.windowObject).scrollLeft();
- $(that.options.windowObject).scrollLeft(xOffset + value * that.options.scrollInputMultiplier * speedFactor);
+ 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);
}
};
@@ -115,26 +133,30 @@ 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 value of the gamepad input (in pixels).
- * @param {Integer} speedFactor - Times by which the scroll speed should be increased.
- * @param {Boolean} invert - Whether the scroll should be in opposite order.
+ * @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).
*
*/
- gamepad.inputMapperUtils.content.scrollVertically = function (that, value, speedFactor, invert) {
- // Get the updated input value according to the configuration.
- var inversionFactor = invert ? -1 : 1;
- value = value * inversionFactor;
- if (value > 0) {
- clearInterval(that.intervalRecords.upwardScroll);
- that.scrollDown(value, speedFactor);
- }
- else if (value < 0) {
- clearInterval(that.intervalRecords.downwardScroll);
- that.scrollUp(-1 * value, speedFactor);
- }
- else {
- clearInterval(that.intervalRecords.upwardScroll);
- clearInterval(that.intervalRecords.downwardScroll);
+ 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);
+ }
}
};
@@ -143,11 +165,14 @@ 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 value of the gamepad input (in pixels).
- * @param {Integer} speedFactor - Times by which the scroll speed should be increased.
+ * @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).
*
*/
- gamepad.inputMapperUtils.content.scrollUp = function (that, value, speedFactor) {
+ 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.
@@ -158,11 +183,16 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
* Scroll the webpage upward only if the input value is more than the cutoff
* value.
*/
- if (value > that.options.cutoffValue) {
+ if (that.model.pageInView && (value > that.options.cutoffValue)) {
// Scroll upward according to the new input value.
that.intervalRecords.upwardScroll = setInterval(function () {
- var yOffset = $(that.options.windowObject).scrollTop();
- $(that.options.windowObject).scrollTop(yOffset - value * that.options.scrollInputMultiplier * speedFactor);
+ 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);
}
};
@@ -172,11 +202,14 @@ 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 value of the gamepad input (in pixels).
- * @param {Integer} speedFactor - Times by which the scroll speed should be increased.
+ * @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).
*
*/
- gamepad.inputMapperUtils.content.scrollDown = function (that, value, speedFactor) {
+ 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.
@@ -187,11 +220,19 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
* Scroll the webpage downward only if the input value is more than the cutoff
* value.
*/
- if (value > that.options.cutoffValue) {
+ if (that.model.pageInView && (value > that.options.cutoffValue)) {
// Scroll upward according to the new input value.
that.intervalRecords.downwardScroll = setInterval(function () {
- var yOffset = $(that.options.windowObject).scrollTop();
- $(that.options.windowObject).scrollTop(yOffset + value * that.options.scrollInputMultiplier * speedFactor);
+ 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);
}
};
@@ -202,28 +243,30 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
*
* @param {Object} that - The inputMapper component.
* @param {Integer} value - The value of the gamepad input.
- * @param {Integer} speedFactor - Times by which the tabbing speed should be increased.
- * @param {Boolean} invert - Whether the tabbing should be in opposite order.
+ * @param {Object} actionOptions - The action options (ex: speedFactor).
*
*/
- gamepad.inputMapperUtils.content.thumbstickTabbing = function (that, value, speedFactor, invert) {
- var inversionFactor = 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, 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
+ );
+ }
}
};
@@ -237,7 +280,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
*
*/
gamepad.inputMapperUtils.content.buttonTabNavigation = function (that, value, direction) {
- if (value > that.options.cutoffValue) {
+ if (that.model.pageInView && (value > that.options.cutoffValue)) {
var length = that.tabbableElements.length;
// Tab only if at least one tabbable element is available.
@@ -247,7 +290,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
* currently focused, shift the focus to the first element. Otherwise
* shift the focus to the next element.
*/
- var activeElement = document.activeElement;
+ var activeElement = that.model.activeModal ? fluid.get(that, "model.shadowElement.activeElement") : document.activeElement;
if (activeElement.nodeName === "BODY" || !activeElement) {
that.tabbableElements[0].focus();
}
@@ -262,20 +305,26 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
activeElementIndex = that.currentTabIndex;
}
+ var increment = 0;
if (direction === "forwardTab") {
- that.currentTabIndex = (activeElementIndex + 1) % length;
+ increment = 1;
}
else if (direction === "reverseTab") {
- /**
- * Move to the first element if the last element on the webpage
- * is focused.
- */
- if (activeElementIndex === 0) {
- activeElementIndex = length;
- }
- that.currentTabIndex = activeElementIndex - 1;
+ increment = -1;
+ }
+
+ activeElement.blur();
+
+ 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.tabbableElements[that.currentTabIndex].focus();
}
}
}
@@ -285,57 +334,88 @@ 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 (value) {
- if (value > 0) {
- /**
- * If SELECT element is currently focused, toggle its state. Otherwise perform
- * the regular click operation.
- */
- if (document.activeElement.nodeName === "SELECT") {
- var optionsLength = 0;
-
- // Compute the number of options and store it.
- document.activeElement.childNodes.forEach(function (childNode) {
- if (childNode.nodeName === "OPTION") {
- optionsLength++;
- }
- });
+ 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;
- // Toggle the SELECT dropdown.
- if (!document.activeElement.getAttribute("size") || document.activeElement.getAttribute("size") === "1") {
- /**
- * Store the initial size of the dropdown in a separate attribute
- * (if specified already).
- */
- var initialSizeString = document.activeElement.getAttribute("size");
- if (initialSizeString) {
- document.activeElement.setAttribute("initialSize", parseInt(initialSizeString));
+ 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();
+
+ 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);
}
+ else {
+ // Obtain the initial size of the dropdown.
+ var sizeString = activeElement.getAttribute("initialSize") || "1";
- /**
- * 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);
- document.activeElement.setAttribute("size", length);
+ // Restore the size of the dropdown.
+ activeElement.setAttribute("size", parseInt(sizeString));
+ }
}
else {
- // Obtain the initial size of the dropdown.
- var sizeString = document.activeElement.getAttribute("initialSize") || "1";
-
- // Restore the size of the dropdown.
- document.activeElement.setAttribute("size", parseInt(sizeString));
+ // Click on the focused element.
+ activeElement.click();
}
}
- else {
- // Click on the focused element.
- document.activeElement.click();
+ }
+ };
+
+ gamepad.inputMapperUtils.content.isTextInput = function (element) {
+ if (element.nodeName === "INPUT") {
+ var allowedTypes = ["text", "search", "email", "password", "tel", "text", "url"];
+ var inputType = element.getAttribute("type");
+ if (!inputType || allowedTypes.includes(inputType)) {
+ return true;
}
}
+ else if (element.nodeName === "TEXTAREA") {
+ return true;
+ }
+
+ return false;
};
/**
@@ -344,19 +424,20 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
*
* @param {Object} that - The inputMapper component.
* @param {Integer} value - The value of the gamepad input.
- * @param {Boolean} invert - Whether the history navigation should be in opposite
- * order.
+ * @param {Object} actionOptions - The action options (ex: invert).
*
*/
- gamepad.inputMapperUtils.content.thumbstickHistoryNavigation = function (that, value, invert) {
- // Get the updated input value according to the configuration.
- var inversionFactor = 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, 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);
+ }
}
};
@@ -374,27 +455,32 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
*
*/
gamepad.inputMapperUtils.content.previousPageInHistory = function (that, value) {
- if (value > that.options.cutoffValue) {
- var activeElementIndex = null;
+ 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);
+ }
- // Get the index of the currently active element, if available.
- if (fluid.get(document, "activeElement")) {
- var tabbableElements = ally.query.tabbable({ strategy: "strict" }).sort(that.tabindexSortFilter);
- 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();
+ });
}
-
- /**
- * 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;
+ else {
+ that.vibrate();
}
- chrome.storage.local.set(storageData, function () {
- that.options.windowObject.history.back();
- });
}
};
@@ -407,47 +493,61 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
*
*/
gamepad.inputMapperUtils.content.nextPageInHistory = function (that, value) {
- if (value > that.options.cutoffValue) {
- var activeElementIndex = null;
+ 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);
+ }
- // Get the index of the currently active element, if available.
- if (fluid.get(document, "activeElement")) {
- var tabbableElements = ally.query.tabbable({ strategy: "strict" }).sort(that.tabindexSortFilter);
- 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();
+ });
}
-
- /**
- * 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;
+ else {
+ that.vibrate();
}
- chrome.storage.local.set(storageData, function () {
- that.options.windowObject.history.forward();
- });
}
};
/**
*
* 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 {String} key - The key (ex: `ArrowLeft`) to simulate.
+ * @param {Object} actionOptions - The options for this action.
+ * @property {String} key - The key (ex: `ArrowLeft`) to simulate.
*
*/
- gamepad.inputMapperUtils.content.sendKey = function (value, key) {
- if (value > 0) {
- var keyDownEvent = new KeyboardEvent("keydown", { key: key, code: key, bubbles: true });
- document.activeElement.dispatchEvent(keyDownEvent);
+ gamepad.inputMapperUtils.content.sendKey = function (that, value, actionOptions) {
+ var key = fluid.get(actionOptions, "key");
+
+ // 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);
- // 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 });
- document.activeElement.dispatchEvent(keyUpEvent);
+ var keyUpEvent = new KeyboardEvent("keyup", { key: key, code: key, bubbles: true });
+ activeElement.dispatchEvent(keyUpEvent);
+ }
}
};
@@ -457,31 +557,33 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
*
* @param {Object} that - The inputMapper component.
* @param {Integer} value - The value of the gamepad input.
- * @param {Integer} speedFactor - Times by which the tabbing speed should be increased.
- * @param {Boolean} invert - Whether the tabbing should be in opposite order.
+ * @param {Object} actionOptions - The action options (ex: speedFactor).
* @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, speedFactor, invert, forwardKey, backwardKey) {
- var inversionFactor = invert ? -1 : 1;
+ 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,
- that.options.frequency * speedFactor,
- value,
- forwardKey
+ gamepad.inputMapperUtils.content.sendKey, // func
+ that.options.frequency * speedFactor, // delay
+ that, // arg 0
+ value, //arg 1
+ { key: forwardKey } // arg 2
);
}
else if (value < (-1 * that.options.cutoffValue)) {
that.intervalRecords[backwardKey] = setInterval(
gamepad.inputMapperUtils.content.sendKey,
that.options.frequency * speedFactor,
+ that,
-1 * value,
- backwardKey
+ { key: backwardKey }
);
}
};
diff --git a/src/js/content_scripts/input-mapper.js b/src/js/content_scripts/input-mapper.js
index 397c348..cd3cc32 100644
--- a/src/js/content_scripts/input-mapper.js
+++ b/src/js/content_scripts/input-mapper.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
@@ -10,270 +10,298 @@ You may obtain a copy of the BSD 3-Clause License at
https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
*/
-/* global chrome, ally */
+/* global chrome */
-(function (fluid, $) {
+(function (fluid) {
"use strict";
var gamepad = fluid.registerNamespace("gamepad");
fluid.registerNamespace("gamepad.inputMapper");
- // TODO: Focus trap in modal, look at Weavly and others.
- // TODO: Close on click outside inner container.
- // TODO: Close on escape.
fluid.defaults("gamepad.inputMapper", {
gradeNames: ["gamepad.inputMapper.base", "fluid.viewComponent"],
- listeners: {
- "onCreate.restoreFocus": "{that}.restoreFocus",
- "onCreate.updateControls": "{that}.updateControls"
+ model: {
+ activeModal: false,
+ shadowElement: false,
+ textInputValue: "",
+ textInputType: "",
+
+ bindings: gamepad.bindings.defaults,
+ prefs: gamepad.prefs.defaults
},
- invokers: {
- // Actions, these are called with: value, speedFactor, invert, background, oldValue, homepageURL
- restoreFocus: {
- funcName: "gamepad.inputMapper.restoreFocus",
- args: ["{that}.options.windowObject", "{that}.tabindexSortFilter"]
+ modelListeners: {
+ pageInView: {
+ func: "gamepad.inputMapper.handlePageInViewChange",
+ args: ["{that}"]
+ },
+ activeModal: {
+ func: "{that}.updateTabbables"
},
- updateControls: {
+ textInputValue: {
+ funcName: "gamepad.inputMapper.updateFormFieldText",
+ args: ["{that}"]
+ }
+ },
+ events: {
+ onWindowFocus: null,
+ onWindowBlur: null,
+ onPageShow: null,
+ onPageHide: null
+ },
+
+ listeners: {
+ // Old persistence.
+ // TODO: Remove
+ "onCreate.updateControls": {
funcName: "gamepad.inputMapper.updateControls",
args: ["{that}"]
},
+
+ "onCreate.loadSettings": {
+ funcName: "gamepad.inputMapper.loadSettings",
+ args: ["{that}"]
+ },
+
+ // Wire up event listeners to window.
+ "onCreate.handleWindowFocus": {
+ funcName: "window.addEventListener",
+ args: ["focus", "{that}.events.onWindowFocus.fire"]
+ },
+ "onCreate.handleWindowBlur": {
+ funcName: "window.addEventListener",
+ args: ["blur", "{that}.events.onWindowBlur.fire"]
+ },
+ "onCreate.handlePageShow": {
+ funcName: "window.addEventListener",
+ args: ["pageshow", "{that}.events.onPageShow.fire"]
+ },
+ "onCreate.handlePageHide": {
+ funcName: "window.addEventListener",
+ args: ["pagehide", "{that}.events.onPageHide.fire"]
+ },
+
+ // Handle window-related events
+ "onWindowFocus": {
+ funcName: "gamepad.inputMapper.handleFocused",
+ args: ["{that}"]
+ },
+ "onPageShow": {
+ funcName: "gamepad.inputMapper.handleFocused",
+ args: ["{that}"]
+ },
+ "onWindowBlur": {
+ funcName: "gamepad.inputMapper.handleBlurred",
+ args: ["{that}"]
+ },
+ "onPageHide": {
+ funcName: "gamepad.inputMapper.handleBlurred",
+ args: ["{that}"]
+ }
+ },
+ invokers: {
+ // Actions, these are called with: value, oldValue, actionOptions
goToPreviousTab: {
- funcName: "gamepad.inputMapperUtils.background.sendMessage",
- args: ["{that}", "goToPreviousTab", "{arguments}.0", "{arguments}.4"]
+ funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown",
+ args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "goToPreviousTab" }]
},
goToNextTab: {
- funcName: "gamepad.inputMapperUtils.background.sendMessage",
- args: ["{that}", "goToNextTab", "{arguments}.0", "{arguments}.4"]
+ funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown",
+ args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "goToNextTab"}]
},
closeCurrentTab: {
- funcName: "gamepad.inputMapperUtils.background.sendMessage",
- args: ["{that}", "closeCurrentTab", "{arguments}.0", "{arguments}.4"]
+ funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown",
+ args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "closeCurrentTab"}]
},
openNewTab: {
- funcName: "gamepad.inputMapperUtils.background.sendMessage",
- args: ["{that}", "openNewTab", "{arguments}.0", "{arguments}.4", "{arguments}.3", "{arguments}.5"]
+ funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown",
+ args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "openNewTab" }]
},
openNewWindow: {
- funcName: "gamepad.inputMapperUtils.background.sendMessage",
- args: ["{that}", "openNewWindow", "{arguments}.0", "{arguments}.4", "{arguments}.3", "{arguments}.5"]
+ funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown",
+ args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "openNewWindow" }]
},
closeCurrentWindow: {
- funcName: "gamepad.inputMapperUtils.background.sendMessage",
- args: ["{that}", "closeCurrentWindow", "{arguments}.0", "{arguments}.4"]
+ funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown",
+ args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "closeCurrentWindow" }]
},
goToPreviousWindow: {
- funcName: "gamepad.inputMapperUtils.background.sendMessage",
- args: ["{that}", "goToPreviousWindow", "{arguments}.0", "{arguments}.4"]
+ funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown",
+ args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "goToPreviousWindow" }]
},
goToNextWindow: {
- funcName: "gamepad.inputMapperUtils.background.sendMessage",
- args: ["{that}", "goToNextWindow", "{arguments}.0", "{arguments}.4"]
+ funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown",
+ args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "goToNextWindow" }]
},
zoomIn: {
- funcName: "gamepad.inputMapperUtils.background.sendMessage",
- args: ["{that}", "zoomIn", "{arguments}.0", "{arguments}.4"]
+ funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown",
+ args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "zoomIn" }]
},
zoomOut: {
- funcName: "gamepad.inputMapperUtils.background.sendMessage",
- args: ["{that}", "zoomOut", "{arguments}.0", "{arguments}.4"]
+ 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"]
},
maximizeWindow: {
- funcName: "gamepad.inputMapperUtils.background.sendMessage",
- args: ["{that}", "maximizeWindow", "{arguments}.0", "{arguments}.4"]
+ funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown",
+ args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "maximizeWindow" }]
},
restoreWindowSize: {
- funcName: "gamepad.inputMapperUtils.background.sendMessage",
- args: ["{that}", "restoreWindowSize", "{arguments}.0", "{arguments}.4"]
+ funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown",
+ args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "restoreWindowSize" }]
},
thumbstickWindowSize: {
funcName: "gamepad.inputMapperUtils.background.thumbstickWindowSize",
- args: ["{that}", "{arguments}.0", "{arguments}.2"]
+ args: ["{that}", "{arguments}.0", "{arguments}.2"] // value, actionOptions
},
reopenTabOrWindow: {
- funcName: "gamepad.inputMapperUtils.background.sendMessage",
- args: ["{that}", "reopenTabOrWindow", "{arguments}.0", "{arguments}.4"]
+ funcName: "gamepad.inputMapperUtils.background.postMessageOnControlDown",
+ args: ["{that}", "{arguments}.0", "{arguments}.1", { actionName: "reopenTabOrWindow" }]
},
openActionLauncher: {
funcName: "gamepad.inputMapper.openActionLauncher",
- args: ["{that}", "{arguments}.0", "{arguments}.4"] // value, oldValue
+ args: ["{that}", "{arguments}.0", "{arguments}.1"] // value, oldValue
+ },
+ openSearchKeyboard: {
+ funcName: "gamepad.inputMapper.openSearchKeyboard",
+ args: ["{that}", "{arguments}.0", "{arguments}.1"] // value, oldValue
+ },
+ openConfigPanel: {
+ funcName: "gamepad.inputMapper.openConfigPanel",
+ args: ["{that}", "{arguments}.0", "{arguments}.1"] // value, oldValue
}
},
- model: {
- hideActionPanelLauncher: true
- },
components: {
- actionLauncher: {
+ modalManager: {
container: "{that}.container",
- type: "gamepad.actionLauncher",
+ type: "gamepad.modalManager",
options: {
model: {
- hidden: "{gamepad.inputMapper}.model.hideActionPanelLauncher",
- lastExternalFocused: "{gamepad.inputMapper}.model.lastExternalFocused"
+ activeModal: "{gamepad.inputMapper}.model.activeModal",
+ lastExternalFocused: "{gamepad.inputMapper}.model.lastExternalFocused",
+ shadowElement: "{gamepad.inputMapper}.model.shadowElement",
+ textInputValue: "{gamepad.inputMapper}.model.textInputValue",
+ textInputType: "{gamepad.inputMapper}.model.textInputType"
}
}
}
}
});
- gamepad.inputMapper.openActionLauncher = function (that, value, oldValue) {
- if (value && !oldValue) {
- that.applier.change("lastExternalFocused", document.activeElement);
-
- that.applier.change("hideActionPanelLauncher", false);
- }
+ gamepad.inputMapper.handleFocused = function (that) {
+ that.applier.change("pageInView", true);
};
- /**
- *
- * Restore the previously focused element on the web page after history navigation or
- * switching tabs/windows.
- *
- * @param {Object} windowObject - The inputMapper component's windowObject option.
- * @param {Function} tabindexSortFilter - The filter to be used for sorting elements
- * based on their tabindex value.
- *
- */
- gamepad.inputMapper.restoreFocus = function (windowObject, tabindexSortFilter) {
- $(document).ready(function () {
- /**
- * Get the index of the previously focused element stored in the local
- * storage.
- */
- var pageAddress = windowObject.location.href;
- chrome.storage.local.get([pageAddress], function (resultObject) {
- // Focus only if some element was focused before the history navigation.
- var activeElementIndex = resultObject[pageAddress];
- if (activeElementIndex && activeElementIndex !== -1) {
- var tabbableElements = ally.query.tabbable({ strategy: "strict" }).sort(tabindexSortFilter),
- activeElement = tabbableElements[activeElementIndex];
- if (activeElement) {
- activeElement.focus();
- }
+ gamepad.inputMapper.handleBlurred = function (that) {
+ that.applier.change("pageInView", false);
+ };
- // Clear the stored index of the active element after usage.
- chrome.storage.local.remove([pageAddress]);
- }
- });
- });
+ gamepad.inputMapper.updateFormFieldText = function (that) {
+ if (that.model.lastExternalFocused && gamepad.inputMapperUtils.content.isTextInput(that.model.lastExternalFocused)) {
+ that.model.lastExternalFocused.value = that.model.textInputValue;
+ }
};
- /**
- *
- * Tracks the page/window visibility state and calls the inputMapper instance manager
- * accordingly.
- *
- * @param {Function} inputMapperManager - The function that handles the instance of
- * the inputMapper component.
- * @param {Object} configurationOptions - The configuration options for the
- * inputMapper component.
- *
- */
- gamepad.visibilityChangeTracker = (function (windowObject) {
- // Assume that the page isn't focused initially.
- var inView = false;
- return function (inputMapperManager, configurationOptions) {
- configurationOptions = configurationOptions || {};
-
- // Track changes to the focus/visibility of the window object.
- windowObject.onfocus = windowObject.onblur = windowObject.onpageshow = windowObject.onpagehide = function (event) {
- /**
- * Call the inputMapper instance manager according to the visibility
- * status of the page/window and update the inView value.
- */
- if (event.type === "focus" || event.type === "pageshow") {
- /**
- * Call the inputMapper instance manager with the "visible" status if
- * the page/window is focused back after switching or when page loads
- * (using inView to verify).
- */
- if (!inView) {
- inputMapperManager("visible", configurationOptions);
- inView = true;
- }
- }
- else if (inView) {
- /**
- * Otherwise, call the inputMapper instance manager with the "hidden"
- * status when the focus/visibility of current window/tab is moved to
- * some other window/tab.
- */
- inputMapperManager("hidden");
- inView = false;
- }
- };
- };
- })(window);
+ 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);
- // TODO: Make this something the input mapper does itself to decide whether
- // it should be active. When not visible, deactivate all modals and stop
- // listening to gamepad input. When visible, start listening. See if there
- // is some way to keep it alive if the window is visible but the dev tools
- // are focused.
- /**
- *
- * Manages the inputMapper instance according to the visibility status of the
- * tab/window.
- *
- * @param {String} visibilityStatus - The visibility status of the tab/window.
- * @param {Object} configurationOptions - The configuration options for the
- * inputMapper component.
- *
- */
- gamepad.inputMapperManager = (function () {
- var inputMapperInstance = null;
-
- return function (visibilityStatus, configurationOptions) {
- configurationOptions = configurationOptions || {};
-
- /**
- * Create an instance of the inputMapper when the tab/window is focused
- * again and start reading gamepad inputs (if any gamepad is connected).
- */
- if (visibilityStatus === "visible") {
- /**
- * TODO: Refactor the approach to use a dynamic component instead of
- * creating components dynamically.
- */
-
- // Pass the configuration options to the inputMapper component.
- // TODO: Reactivate a "dormant" inputMapper instance here.
- inputMapperInstance = gamepad.inputMapper("body", configurationOptions);
- inputMapperInstance.events.onGamepadConnected.fire();
- }
- else if (visibilityStatus === "hidden" && inputMapperInstance !== null) {
- /**
- * Destroy the instance of the inputMapper in the current tab when another
- * window/tab is focused or opened.
- */
- // TODO: Manage this better, it makes inspecting HTML really hard.
- // TODO: Make the input mapper "dormant".
- inputMapperInstance.destroy();
+ that.applier.change("activeModal", modalKey);
}
};
- })();
+ };
+ // We do this in a funky way because of our fixed method signature across actions.
+ 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.handlePageInViewChange = function (that) {
+ if (that.model.pageInView) {
+ gamepad.inputMapper.updateControls(that);
+ }
+ else {
+ that.applier.change("activeModal", false);
+ }
+ };
+
+ // TODO: Remove this once the new bindings are fully wired up.
/**
*
- * Update the gamepad configuration if a custom configuration is available.
+ * (Re)load the gamepad configuration from local storage.
*
* @param {Object} that - The inputMapper component.
*
*/
gamepad.inputMapper.updateControls = function (that) {
- chrome.storage.local.get(["gamepadConfiguration"], function (configWrapper) {
- var gamepadConfig = configWrapper.gamepadConfiguration;
+ 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);
- // Update the gamepad configuration only if it's available.
- if (gamepadConfig) {
- that.applier.change("map", gamepadConfig);
+ /*
+ The two params for the onChanged listener callbak 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: {}}
+ */
+
+ chrome.storage.onChanged.addListener(function (changes) {
+ if (changes["gamepad-prefs"]) {
+ gamepad.inputMapper.loadPrefs(that);
+ }
+
+ if (changes["gamepad-bindings"]) {
+ gamepad.inputMapper.loadBindings(that);
}
});
};
- gamepad.visibilityChangeTracker(gamepad.inputMapperManager);
+ gamepad.inputMapper.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.inputMapper.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.inputMapperInstance = gamepad.inputMapper("body");
})(fluid, jQuery);
diff --git a/src/js/content_scripts/modal.js b/src/js/content_scripts/modal.js
index 9a0ec45..d490afb 100644
--- a/src/js/content_scripts/modal.js
+++ b/src/js/content_scripts/modal.js
@@ -23,9 +23,10 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
fluid.defaults("gamepad.modal", {
gradeNames: ["gamepad.templateRenderer"],
model: {
- hidden: true,
- lastExternalFocused: false
+ classNames: "",
+ hidden: true
},
+ icon: gamepad.svg["gamepad-icon"],
modelListeners: {
hidden: [
{
@@ -43,18 +44,18 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
icon: ".modal-icon",
innerContainer: ".modal-inner-container",
modalBody: ".modal-body",
+ modalFooter: ".modal-footer",
modalCloseButton: ".modal-close-button",
leadingFocusTrap: ".modal-focus-trap-leading",
trailingFocusTrap: ".modal-focus-trap-trailing"
},
markup: {
- // TODO: Add the ability to retrieve our icon URL and display the icon onscreen.
- container: ""
+ container: ""
},
invokers: {
closeModal: {
funcName: "gamepad.modal.closeModal",
- args: ["{that}", "{arguments}.0"] // event
+ args: ["{gamepad.modalManager}", "{arguments}.0"] // event
},
handleKeydown: {
funcName: "gamepad.modal.handleKeydown",
@@ -74,15 +75,11 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
args: ["{that}", true]
}
},
- // TODO: Relay hidden classname to control visibility.
- // TODO: clicking outside the inner container should close the modal.
- // We can't use this form because we also need to restore focus.
- // modelRelay: {
- // source: "{that}.model.dom.outerContainer.click",
- // target: "{that}.model.hidden",
- // singleTransform: "fluid.transforms.toggle"
- // },
listeners: {
+ "onCreate.drawIcon": {
+ funcName: "gamepad.modal.drawIcon",
+ args: ["{that}"]
+ },
"onCreate.bindOuterContainerClick": {
this: "{that}.container",
method: "click",
@@ -134,11 +131,21 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
}
};
- gamepad.modal.closeModal = function (that, event) {
+ gamepad.modal.drawIcon = function (that) {
+ var iconElement = that.locate("icon");
+ iconElement.html(that.options.icon);
+ };
+
+ /**
+ *
+ * @param {Object} modalManager - The modal manager component.
+ * @param {Event} event - The event to which we are responding.
+ */
+ gamepad.modal.closeModal = function (modalManager, event) {
event.preventDefault();
- that.applier.change("hidden", true);
- if (that.model.lastExternalFocused && that.model.lastExternalFocused.focus) {
- that.model.lastExternalFocused.focus();
+ modalManager.applier.change("activeModal", false);
+ if (modalManager.model.lastExternalFocused && modalManager.model.lastExternalFocused.focus) {
+ modalManager.model.lastExternalFocused.focus();
}
};
@@ -156,8 +163,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
var innerContainer = that.locate("innerContainer");
// Search for tabbables, focus on first or last element depending.
- // TODO: Repurpose sorting from other areas where we use ally?
- var tabbableElements = ally.query.tabbable({ context: innerContainer, strategy: "strict" });
+ var tabbableElements = ally.query.tabsequence({ context: innerContainer, strategy: "strict" });
if (tabbableElements.length) {
var elementIndex = reverse ? tabbableElements.length - 1 : 0;
var elementToFocus = tabbableElements[elementIndex];
diff --git a/src/js/content_scripts/modalManager.js b/src/js/content_scripts/modalManager.js
new file mode 100644
index 0000000..d4d24e7
--- /dev/null
+++ b/src/js/content_scripts/modalManager.js
@@ -0,0 +1,112 @@
+/*
+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.modalManager", {
+ gradeNames: ["gamepad.templateRenderer"],
+ markup: {
+ container: "
",
+ styles: ""
+ },
+ model: {
+ activeModal: false,
+ shadowElement: false,
+ lastExternalFocused: false,
+ textInputValue: "",
+ textInputType: "",
+
+ // Inline all styles from JS-wrapped global namespaced variable.
+ styles: gamepad.css
+ },
+ events: {
+ onShadowReady: null
+ },
+ components: {
+ actionLauncher: {
+ container: "{that}.model.shadowElement",
+ type: "gamepad.actionLauncher",
+ createOnEvent: "onShadowReady",
+ options: {
+ model: {
+ hidden: "{gamepad.modalManager}.model.hideActionLauncher"
+ }
+ }
+ },
+ onscreenKeyboard: {
+ container: "{that}.model.shadowElement",
+ type: "gamepad.osk.modal",
+ createOnEvent: "onShadowReady",
+ options: {
+ model: {
+ hidden: "{gamepad.modalManager}.model.hideOnscreenKeyboard",
+ textInputValue: "{gamepad.modalManager}.model.textInputValue"
+ }
+ }
+ },
+ searchKeyboard: {
+ container: "{that}.model.shadowElement",
+ type: "gamepad.searchKeyboard.modal",
+ createOnEvent: "onShadowReady",
+ options: {
+ model: {
+ hidden: "{gamepad.modalManager}.model.hideSearchKeyboard"
+ }
+ }
+ }
+ },
+ listeners: {
+ "onCreate.createShadow": {
+ funcName: "gamepad.modalManager.createShadow",
+ args: ["{that}"]
+ }
+ },
+ modelListeners: {
+ activeModal: {
+ excludeSource: "init",
+ funcName: "gamepad.modalManager.toggleModals",
+ args: ["{that}"]
+ }
+ }
+ });
+
+ gamepad.modalManager.createShadow = function (that) {
+ var host = that.container[0];
+ var shadowElement = host.attachShadow({mode: "open"});
+
+ // We inline all styles here so that all modals get the common styles,
+ // and to avoid managing multiple shadow elements.
+ shadowElement.innerHTML = fluid.stringTemplate(that.options.markup.styles, that.model);
+
+ that.applier.change("shadowElement", shadowElement);
+ that.events.onShadowReady.fire();
+ };
+
+ gamepad.modalManager.toggleModals = function (that) {
+ var transaction = that.applier.initiate();
+ var hideActionLauncher = that.model.activeModal !== "actionLauncher";
+ transaction.fireChangeRequest({ path: "hideActionLauncher", value: hideActionLauncher });
+
+ var hideOnscreenKeyboard = that.model.activeModal !== "onscreenKeyboard";
+ transaction.fireChangeRequest({ path: "hideOnscreenKeyboard", value: hideOnscreenKeyboard });
+
+
+ var hideSearchKeyboard = that.model.activeModal !== "searchKeyboard";
+ transaction.fireChangeRequest({ path: "hideSearchKeyboard", value: hideSearchKeyboard });
+
+ transaction.commit();
+ };
+})(fluid);
diff --git a/src/js/content_scripts/onscreen-keyboard.js b/src/js/content_scripts/onscreen-keyboard.js
new file mode 100644
index 0000000..e26bf9d
--- /dev/null
+++ b/src/js/content_scripts/onscreen-keyboard.js
@@ -0,0 +1,119 @@
+/*
+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
+*/
+
+/*
+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");
+
+ // Adapted from: https://github.com/duhrer/fluid-osk/blob/GH-1/examples/js/customTextInput.js
+
+ fluid.defaults("gamepad.osk.modal", {
+ gradeNames: ["gamepad.modal"],
+ model: {
+ classNames: " onscreen-keyboard-modal",
+ label: "Gamepad Navigator: Onscreen Keyboard",
+ lastExternalFocused: false,
+ textInputValue: ""
+ },
+ components: {
+ input: {
+ container: "{that}.dom.modalBody",
+ type: "osk.inputs.text",
+ options: {
+ model: {
+ composition: "{gamepad.osk.modal}.model.textInputValue"
+ },
+ modelListeners: {
+ composition: {
+ excludeSource: "local",
+ funcName: "gamepad.osk.modal.updateTextInputWithExternalValue",
+ args: ["{that}"]
+ }
+ }
+ }
+ },
+ osk: {
+ container: "{that}.dom.modalBody",
+ type: "gamepad.osk.keyboard",
+ options: {
+ listeners: {
+ "onAction.updateInput": {
+ priority: "before:handleLatches",
+ funcName: "gamepad.osk.modal.processAction",
+ args: ["{that}", "{osk.inputs.text}", "{arguments}.0"] // inputComponent, actionDef
+ }
+ }
+ }
+ }
+ }
+ });
+
+ // Our assumptions are slightly different than fluid-osk, so we hack in an override to pass in external values.
+ // It may be possible to move this to fluid-osk, but in testing this causes problems with other external updates,
+ // i.e. those made indirectly via addChar and other invokers.
+ gamepad.osk.modal.updateTextInputWithExternalValue = function (inputComponent) {
+ var transaction = inputComponent.applier.initiate();
+ transaction.fireChangeRequest({ path: "cursorIndex", value: inputComponent.model.composition.length});
+ transaction.fireChangeRequest({ path: "beforeCursor", value: inputComponent.model.composition });
+ transaction.fireChangeRequest({ path: "afterCursor", value: "" });
+ transaction.commit();
+ };
+
+ gamepad.osk.modal.processAction = function (keyboardComponent, inputComponent, actionDef) {
+ if (actionDef.action === "text") {
+ var toAdd = actionDef.payload;
+ if (keyboardComponent.model.latchedKeys.ShiftLeft || keyboardComponent.model.latchedKeys.ShiftRight ) {
+ toAdd = actionDef.shiftPayload;
+ }
+ else if (keyboardComponent.model.latchedKeys.CapsLock) {
+ toAdd = actionDef.capsPayload;
+ }
+ var isInsert = keyboardComponent.model.latchedKeys.Insert;
+ inputComponent.addChar(toAdd, isInsert);
+ }
+ else if (actionDef.action === "backspace") {
+ inputComponent.removePreviousChar();
+ }
+ else if (actionDef.action === "delete") {
+ inputComponent.removeNextChar();
+ }
+ else if (actionDef.action === "up" || actionDef.action === "left") {
+ inputComponent.moveCursor(-1);
+ }
+ else if (actionDef.action === "down" || actionDef.action === "right") {
+ inputComponent.moveCursor(1);
+ }
+ else if (actionDef.action === "home") {
+ inputComponent.moveCursorToStart();
+ }
+ else if (actionDef.action === "end") {
+ inputComponent.moveCursorToEnd();
+ }
+ };
+
+ // Wrapped in case we want to extend later, for example to add bespoke
+ // buttons for domains.
+ fluid.defaults("gamepad.osk.keyboard", {
+ gradeNames: ["osk.keyboard.qwerty"]
+ });
+})(fluid);
diff --git a/src/js/content_scripts/search-keyboard.js b/src/js/content_scripts/search-keyboard.js
new file mode 100644
index 0000000..cd6c0c9
--- /dev/null
+++ b/src/js/content_scripts/search-keyboard.js
@@ -0,0 +1,81 @@
+/*
+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
+*/
+
+/*
+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.searchKeyboard.searchButton", {
+ gradeNames: ["gamepad.templateRenderer"],
+ markup: {
+ container: "Search "
+ }
+ });
+
+ fluid.defaults("gamepad.searchKeyboard.modal", {
+ gradeNames: ["gamepad.osk.modal"],
+ model: {
+ label: "Gamepad Navigator: Search",
+ classNames: " gamepad-navigator-searchKeyboard"
+ },
+
+ invokers: {
+ handleSearchButtonClick: {
+ funcName: "gamepad.searchKeyboard.modal.handleSearchButtonClick",
+ args: ["{that}", "{inputMapper}", "{arguments}.0"]
+ }
+ },
+
+ components: {
+ searchButton: {
+ container: "{that}.dom.modalFooter",
+ type: "gamepad.searchKeyboard.searchButton",
+ options: {
+ listeners: {
+ "onCreate.bindClick": {
+ this: "{searchButton}.container",
+ method: "click",
+ args: ["{gamepad.searchKeyboard.modal}.handleSearchButtonClick"]
+ }
+ }
+ }
+ }
+ }
+ });
+
+ gamepad.searchKeyboard.modal.handleSearchButtonClick = function (that, inputMapper, event) {
+ that.applier.change("activeModal", false);
+ event.preventDefault();
+
+ if (that.model.textInputValue && that.model.textInputValue.trim().length) {
+ var actionOptions = {
+ actionName: "search",
+ // See: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/search/query
+ disposition: "NEW_TAB",
+ text: that.model.textInputValue.trim()
+ };
+
+ gamepad.inputMapperUtils.background.postMessage(inputMapper, actionOptions);
+ }
+ };
+})(fluid);
diff --git a/src/js/shared/templateRenderer.js b/src/js/content_scripts/templateRenderer.js
similarity index 72%
rename from src/js/shared/templateRenderer.js
rename to src/js/content_scripts/templateRenderer.js
index 227eaab..31e2d94 100644
--- a/src/js/shared/templateRenderer.js
+++ b/src/js/content_scripts/templateRenderer.js
@@ -6,6 +6,18 @@ 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
+*/
+
+/*
+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
*/
diff --git a/src/js/content_scripts/utils.js b/src/js/content_scripts/utils.js
new file mode 100644
index 0000000..9e6178b
--- /dev/null
+++ b/src/js/content_scripts/utils.js
@@ -0,0 +1,83 @@
+/*
+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
+*/
+
+/*
+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.registerNamespace("gamepad.utils");
+
+ gamepad.utils.isDeeplyEqual = function (firstThing, secondThing) {
+ if (typeof firstThing !== typeof secondThing) {
+ return false;
+ }
+ else if (Array.isArray(firstThing)) {
+ if (firstThing.length === secondThing.length) {
+ for (var arrayIndex = 0; arrayIndex < firstThing.length; arrayIndex++) {
+ var arrayItemsEqual = gamepad.utils.isDeeplyEqual(firstThing[arrayIndex], secondThing[arrayIndex]);
+ if (!arrayItemsEqual) { return false; }
+ }
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+ else if (typeof firstThing === "object") {
+ var firstThingKeys = Object.keys(firstThing);
+ var secondThingKeys = Object.keys(secondThing);
+
+ if (firstThingKeys.length !== secondThingKeys.length) {
+ return false;
+ }
+
+ for (var keyIndex = 0; keyIndex < firstThingKeys.length; keyIndex++) {
+ var key = firstThingKeys[keyIndex];
+ var objectPropertiesEqual = gamepad.utils.isDeeplyEqual(firstThing[key], secondThing[key]);
+ if (!objectPropertiesEqual) { return false; }
+ }
+
+ return true;
+ }
+ else {
+ return firstThing === secondThing;
+ }
+ };
+
+ gamepad.utils.getStoredKey = function (key) {
+ var storagePromise = new Promise(function (resolve) {
+ // Apparently, we have to use a callback and can't use `await` unless we make our own wrapper.
+ chrome.storage.local.get([key], function (storedObject) {
+ if (storedObject[key]) {
+ resolve(storedObject[key]);
+ }
+ else {
+ resolve(false);
+ }
+ });
+ });
+ return storagePromise;
+ };
+
+})(fluid);
diff --git a/src/js/settings/settings.js b/src/js/settings/settings.js
new file mode 100644
index 0000000..a0e37e2
--- /dev/null
+++ b/src/js/settings/settings.js
@@ -0,0 +1,441 @@
+/*
+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: "%label "
+ },
+ 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: ""
+ },
+ 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/toggle.js b/src/js/settings/toggle.js
new file mode 100644
index 0000000..b76caca
--- /dev/null
+++ b/src/js/settings/toggle.js
@@ -0,0 +1,45 @@
+/*
+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.toggle", {
+ gradeNames: ["gamepad.templateRenderer"],
+ styles: {
+ checked: "checked"
+ },
+ model: {
+ label: "Toggle",
+ checked: true
+ },
+ markup: {
+ container: ""
+ },
+ selectors: {
+ toggle: ".gamepad-toggle"
+ },
+ modelRelay: [
+ {
+ source: "{that}.model.dom.toggle.click",
+ target: "{that}.model.checked",
+ singleTransform: "fluid.transforms.toggle"
+ },
+ {
+ source: "{that}.model.checked",
+ target: {
+ segs: ["dom", "toggle", "class", "{that}.options.styles.checked"]
+ }
+ }
+ ]
+ });
+})(fluid);
diff --git a/src/js/shared/configuration-maps.js b/src/js/shared/configuration-maps.js
index 4b8ba8e..2da88f6 100644
--- a/src/js/shared/configuration-maps.js
+++ b/src/js/shared/configuration-maps.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
diff --git a/src/js/shared/prefs.js b/src/js/shared/prefs.js
new file mode 100644
index 0000000..58bbd1f
--- /dev/null
+++ b/src/js/shared/prefs.js
@@ -0,0 +1,34 @@
+/*
+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
+*/
+
+/*
+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.prefs");
+ gamepad.prefs.defaults = {
+ analogCutoff: 0.25,
+ newTabOrWindowURL: "https://www.google.com/",
+ openWindowOnStartup: true,
+ vibrate: true
+ };
+})(fluid);
diff --git a/src/manifest.json b/src/manifest.json
index 23ee38a..649d790 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -1,32 +1,44 @@
{
"name": "Gamepad Navigator",
- "version": "0.3.0",
+ "version": "1.0.0",
"description": "A Chrome extension that allows you to navigate web pages and Chromium-based browsers using a game controller.",
- "author": "Divyanshu Mahajan",
+ "author": "The Gamepad Navigator Authors",
"manifest_version": 3,
"permissions": [
"storage",
"tabs",
- "sessions"
+ "sessions",
+ "search"
],
"content_scripts": [
{
"matches": [
""
],
- "css": [
- "css/common.css",
- "css/modal.css",
- "css/action-launcher.css"
- ],
+ "css": [],
"js": [
"js/lib/infusion/infusion-all.js",
"js/lib/ally/ally.min.js",
+ "js/lib/fluid-osk/templateRenderer.js",
+ "js/lib/fluid-osk/keydefs.js",
+ "js/lib/fluid-osk/key.js",
+ "js/lib/fluid-osk/row.js",
+ "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/templateRenderer.js",
+ "js/shared/prefs.js",
+ "js/content_scripts/bindings.js",
+ "js/content_scripts/styles.js",
+ "js/content_scripts/svgs.js",
+ "js/content_scripts/templateRenderer.js",
"js/content_scripts/modal.js",
"js/content_scripts/action-launcher.js",
+ "js/content_scripts/onscreen-keyboard.js",
+ "js/content_scripts/search-keyboard.js",
+ "js/content_scripts/modalManager.js",
"js/content_scripts/input-mapper-content-utils.js",
"js/content_scripts/input-mapper-background-utils.js",
"js/content_scripts/input-mapper-base.js",
@@ -44,15 +56,16 @@
"128": "images/gamepad-icon-128px.png"
},
"action": {
- "default_icon": "images/gamepad-icon.png",
- "default_popup": "html/configuration-panel.html",
- "default_title": "Open the gamepad configuration panel."
+ "default_icon": {
+ "16": "images/gamepad-icon-16px.png",
+ "32": "images/gamepad-icon-32px.png",
+ "48": "images/gamepad-icon-48px.png",
+ "128": "images/gamepad-icon-128px.png"
+ },
+ "default_title": "Open Gamepad Navigator settings."
},
- "commands": {
- "_execute_action": {
- "suggested_key": {
- "default": "Alt+Shift+G"
- }
- }
+ "options_ui": {
+ "open_in_tab": true,
+ "page": "html/settings.html"
}
}
diff --git a/templates/LICENSE-banner.txt b/templates/LICENSE-banner.txt
index 292cb62..474bb93 100644
--- a/templates/LICENSE-banner.txt
+++ b/templates/LICENSE-banner.txt
@@ -1,4 +1,4 @@
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
diff --git a/tests/html/text-input.html b/tests/html/text-input.html
new file mode 100644
index 0000000..2706223
--- /dev/null
+++ b/tests/html/text-input.html
@@ -0,0 +1,41 @@
+
+
+
+ Text Input
+
+
+ A sample page to try the new onscreen keyboard with various text inputs.
+
+ Input with No Type
+
+
+
+ Text Input
+
+
+
+ Search Input
+
+
+
+ Email Input
+
+
+
+ Password Input
+
+
+
+ Telephone Number Input
+
+
+
+ URL input
+
+
+
+ Text Area
+
+
+
+
diff --git a/tests/js/arrows/arrows-mock-utils.js b/tests/js/arrows/arrows-mock-utils.js
index ab5d925..aae7e4d 100644
--- a/tests/js/arrows/arrows-mock-utils.js
+++ b/tests/js/arrows/arrows-mock-utils.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
diff --git a/tests/js/arrows/arrows-tests.js b/tests/js/arrows/arrows-tests.js
index 78bb395..ac8359a 100644
--- a/tests/js/arrows/arrows-tests.js
+++ b/tests/js/arrows/arrows-tests.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
diff --git a/tests/js/click/click-basic-tests.js b/tests/js/click/click-basic-tests.js
index ddfbabc..66c3149 100644
--- a/tests/js/click/click-basic-tests.js
+++ b/tests/js/click/click-basic-tests.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
@@ -15,6 +15,11 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
(function (fluid, $) {
"use strict";
+ /**
+ * TODO: Add tests for links and other elements that involve navigation
+ * between pages.
+ */
+
$(document).ready(function () {
fluid.registerNamespace("gamepad");
diff --git a/tests/js/click/click-dropdown-tests.js b/tests/js/click/click-dropdown-tests.js
index 6fbb0ef..2680a76 100644
--- a/tests/js/click/click-dropdown-tests.js
+++ b/tests/js/click/click-dropdown-tests.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
diff --git a/tests/js/click/click-mock-utils.js b/tests/js/click/click-mock-utils.js
index 0b3629a..cfa6267 100644
--- a/tests/js/click/click-mock-utils.js
+++ b/tests/js/click/click-mock-utils.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
diff --git a/tests/js/gamepad-mock.js b/tests/js/gamepad-mock.js
index bb08027..4977003 100644
--- a/tests/js/gamepad-mock.js
+++ b/tests/js/gamepad-mock.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
diff --git a/tests/js/scroll/scroll-bidirectional-diagonal-tests.js b/tests/js/scroll/scroll-bidirectional-diagonal-tests.js
index a3ab184..f5331cb 100644
--- a/tests/js/scroll/scroll-bidirectional-diagonal-tests.js
+++ b/tests/js/scroll/scroll-bidirectional-diagonal-tests.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
diff --git a/tests/js/scroll/scroll-bidirectional-oneaxes-tests.js b/tests/js/scroll/scroll-bidirectional-oneaxes-tests.js
index bc10605..c1f931f 100644
--- a/tests/js/scroll/scroll-bidirectional-oneaxes-tests.js
+++ b/tests/js/scroll/scroll-bidirectional-oneaxes-tests.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
diff --git a/tests/js/scroll/scroll-mock-utils.js b/tests/js/scroll/scroll-mock-utils.js
index 51ed2be..05503ba 100644
--- a/tests/js/scroll/scroll-mock-utils.js
+++ b/tests/js/scroll/scroll-mock-utils.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
diff --git a/tests/js/scroll/scroll-unidirectional-axes-tests.js b/tests/js/scroll/scroll-unidirectional-axes-tests.js
index d139d88..a816eb0 100644
--- a/tests/js/scroll/scroll-unidirectional-axes-tests.js
+++ b/tests/js/scroll/scroll-unidirectional-axes-tests.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
diff --git a/tests/js/scroll/scroll-unidirectional-button-tests.js b/tests/js/scroll/scroll-unidirectional-button-tests.js
index eaf9582..1152495 100644
--- a/tests/js/scroll/scroll-unidirectional-button-tests.js
+++ b/tests/js/scroll/scroll-unidirectional-button-tests.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
diff --git a/tests/js/tab/tab-axes-tests.js b/tests/js/tab/tab-axes-tests.js
index f71abf6..b7a9603 100644
--- a/tests/js/tab/tab-axes-tests.js
+++ b/tests/js/tab/tab-axes-tests.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
diff --git a/tests/js/tab/tab-button-continuous-tests.js b/tests/js/tab/tab-button-continuous-tests.js
index dba516a..2686c4d 100644
--- a/tests/js/tab/tab-button-continuous-tests.js
+++ b/tests/js/tab/tab-button-continuous-tests.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
diff --git a/tests/js/tab/tab-button-discrete-tests.js b/tests/js/tab/tab-button-discrete-tests.js
index bbad86d..511a6d3 100644
--- a/tests/js/tab/tab-button-discrete-tests.js
+++ b/tests/js/tab/tab-button-discrete-tests.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
diff --git a/tests/js/tab/tab-mock-utils.js b/tests/js/tab/tab-mock-utils.js
index 28849a7..371732d 100644
--- a/tests/js/tab/tab-mock-utils.js
+++ b/tests/js/tab/tab-mock-utils.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
diff --git a/tests/js/tab/tab-order-tests.js b/tests/js/tab/tab-order-tests.js
index 59b6167..6f15449 100644
--- a/tests/js/tab/tab-order-tests.js
+++ b/tests/js/tab/tab-order-tests.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
@@ -21,11 +21,12 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
jqUnit.module("Gamepad Navigator Tab Navigation DOM Tests", {
setup: function () {
+ var testElementContainer = $(".test-elements");
/**
* Get the list of tabbable elements in the order of their tabindex for
* verification.
*/
- gamepad.tests.sortedTabbableElements = ally.query.tabbable({ strategy: "strict" }).sort(gamepad.inputMapper.base.tabindexSortFilter);
+ gamepad.tests.tabbableElements = ally.query.tabsequence({ context: testElementContainer, strategy: "strict" });
}
});
@@ -33,7 +34,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
jqUnit.expect(1);
// Get the first element from the tabbable elements list.
- var firstTabbableElement = gamepad.tests.sortedTabbableElements[0],
+ var firstTabbableElement = gamepad.tests.tabbableElements[0],
hasTabindexOne = firstTabbableElement.getAttribute("tabindex") === "1";
jqUnit.assertTrue("The first element should have tabindex=\"1\"", hasTabindexOne);
@@ -43,7 +44,7 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
jqUnit.expect(1);
// Get the last element from the tabbable elements list.
- var lastTabbableElement = gamepad.tests.sortedTabbableElements[gamepad.tests.sortedTabbableElements.length - 1],
+ var lastTabbableElement = gamepad.tests.tabbableElements[gamepad.tests.tabbableElements.length - 1],
isLastElement = lastTabbableElement.getAttribute("id") === "last";
jqUnit.assertTrue("The first element should be the last div with id=\"last\"", isLastElement);
@@ -54,12 +55,12 @@ https://github.com/fluid-lab/gamepad-navigator/blob/master/LICENSE
// Check if the tabbable elements list contains elements with tabindex="-1".
var elementWithNegativeTabindex = $("button[tabindex=\"-1\"]"),
- hasElementWithNegativeTabindex = gamepad.tests.sortedTabbableElements.includes(elementWithNegativeTabindex[0]);
+ hasElementWithNegativeTabindex = gamepad.tests.tabbableElements.includes(elementWithNegativeTabindex[0]);
jqUnit.assertFalse("The sorted elements array should not contain elements with tabindex=\"-1\"", hasElementWithNegativeTabindex);
// Check if the tabbable elements list contains elements with tabindex="0".
var elementWithTabindexZero = $("div[tabindex=\"0\"]"),
- hasElementWithTabindexZero = gamepad.tests.sortedTabbableElements.includes(elementWithTabindexZero[0]);
+ hasElementWithTabindexZero = gamepad.tests.tabbableElements.includes(elementWithTabindexZero[0]);
jqUnit.assertTrue("The sorted elements array should contain elements with tabindex=\"0\"", hasElementWithTabindexZero);
});
});
diff --git a/tests/js/test-utils.js b/tests/js/test-utils.js
index c60940c..96d633a 100644
--- a/tests/js/test-utils.js
+++ b/tests/js/test-utils.js
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2020 The Gamepad Navigator Authors
+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.
diff --git a/utils/bundleCss.js b/utils/bundleCss.js
new file mode 100644
index 0000000..a24566f
--- /dev/null
+++ b/utils/bundleCss.js
@@ -0,0 +1,56 @@
+/* eslint-env node */
+"use strict";
+var fluid = require("infusion");
+var fs = require("fs");
+var path = require("path");
+
+var gamepad = fluid.registerNamespace("gamepad");
+
+fluid.defaults("gamepad.bundleCss", {
+ gradeNames: ["fluid.component"],
+ outputPath: "dist/js/content_scripts/styles.js",
+ outputTemplate: "\"use strict\";\nvar gamepad = fluid.registerNamespace(\"gamepad\");\ngamepad.css = `%payload`;\n",
+ pathsToBundle: {
+ src: "src/css",
+ osk: "node_modules/fluid-osk/src/css"
+ },
+ excludes: [
+ "src/css/common.css",
+ "src/css/configuration-panel.css"
+ ],
+ listeners: {
+ "onCreate.processDirs": {
+ funcName: "gamepad.bundleCss.processDirs",
+ args: ["{that}"]
+ }
+ }
+});
+
+gamepad.bundleCss.processDirs = function (that) {
+ var payload = "";
+
+ var resolvedExcludes = that.options.excludes.map(function (relativePath) {
+ return path.resolve(relativePath);
+ });
+
+ fluid.each(that.options.pathsToBundle, function (pathToBundle) {
+ var filenames = fs.readdirSync(pathToBundle);
+ fluid.each(filenames, function (filename) {
+ if (filename.toLowerCase().endsWith(".css")) {
+ var filePath = path.resolve(pathToBundle, filename);
+ if (!resolvedExcludes.includes(filePath)) {
+ var fileContents = fs.readFileSync(filePath, { encoding: "utf8"});
+ payload += fileContents;
+ }
+ }
+ });
+ });
+
+ var bundle = fluid.stringTemplate(that.options.outputTemplate, { payload: payload});
+
+ fs.writeFileSync(that.options.outputPath, bundle, { encoding: "utf8"});
+
+ fluid.log(fluid.logLevel.WARN, "bundled all CSS to '" + that.options.outputPath + "'");
+};
+
+gamepad.bundleCss();
diff --git a/utils/bundleSvgs.js b/utils/bundleSvgs.js
new file mode 100644
index 0000000..abbb547
--- /dev/null
+++ b/utils/bundleSvgs.js
@@ -0,0 +1,55 @@
+/* eslint-env node */
+"use strict";
+var fluid = require("infusion");
+var fs = require("fs");
+var path = require("path");
+
+var gamepad = fluid.registerNamespace("gamepad");
+
+fluid.defaults("gamepad.bundleSvgs", {
+ gradeNames: ["fluid.component"],
+ outputPath: "dist/js/content_scripts/svgs.js",
+ outputTemplate: "\"use strict\";\nvar gamepad = fluid.registerNamespace(\"gamepad\");\nfluid.registerNamespace(\"gamepad.svg\");\n%payload\n",
+ singleFileTemplate: "\ngamepad.svg[\"%filename\"] = `%svgText`;\n",
+ pathsToBundle: {
+ src: "src/images"
+ },
+ excludes: [],
+ listeners: {
+ "onCreate.processDirs": {
+ funcName: "gamepad.bundleSvgs.processDirs",
+ args: ["{that}"]
+ }
+ }
+});
+
+gamepad.bundleSvgs.processDirs = function (that) {
+ var payload = "";
+
+ var resolvedExcludes = that.options.excludes.map(function (relativePath) {
+ return path.resolve(relativePath);
+ });
+
+ fluid.each(that.options.pathsToBundle, function (pathToBundle) {
+ var filenames = fs.readdirSync(pathToBundle);
+ fluid.each(filenames, function (filename) {
+ if (filename.toLowerCase().endsWith(".svg")) {
+ var filenameMinusExtension = path.basename(filename, ".svg");
+ var filePath = path.resolve(pathToBundle, filename);
+ if (!resolvedExcludes.includes(filePath)) {
+ var fileContents = fs.readFileSync(filePath, { encoding: "utf8"});
+ var filePayload = fluid.stringTemplate(that.options.singleFileTemplate, { filename: filenameMinusExtension, svgText: fileContents});
+ payload += filePayload;
+ }
+ }
+ });
+ });
+
+ var bundle = fluid.stringTemplate(that.options.outputTemplate, { payload: payload});
+
+ fs.writeFileSync(that.options.outputPath, bundle, { encoding: "utf8"});
+
+ fluid.log(fluid.logLevel.WARN, "bundled all SVG files to '" + that.options.outputPath + "'");
+};
+
+gamepad.bundleSvgs();