{label && {label}}
diff --git a/www/src/Data/Buttons.js b/www/src/Data/Buttons.js
index 8e6ba0827..3b695a8c4 100644
--- a/www/src/Data/Buttons.js
+++ b/www/src/Data/Buttons.js
@@ -323,6 +323,12 @@ const KEYBOARD_LAYOUT = [
['R1', 'R2'],
['L1', 'L2'],
];
+export const DPAD_MASKS = [
+ { label: 'Up', value: 1 << 0 },
+ { label: 'Down', value: 1 << 1 },
+ { label: 'Left', value: 1 << 2 },
+ { label: 'Right', value: 1 << 3 },
+];
export const BUTTON_LAYOUTS = [
{
@@ -384,7 +390,6 @@ export const BUTTON_LAYOUTS = [
];
export const BUTTON_MASKS = [
- { label: 'None', value: 0 },
{ label: 'B1', value: 1 << 0 },
{ label: 'B2', value: 1 << 1 },
{ label: 'B3', value: 1 << 2 },
@@ -399,6 +404,11 @@ export const BUTTON_MASKS = [
{ label: 'R3', value: 1 << 11 },
{ label: 'A1', value: 1 << 12 },
{ label: 'A2', value: 1 << 13 },
+];
+
+export const BUTTON_MASKS_OPTIONS = [
+ { label: 'None', value: 0 },
+ ...BUTTON_MASKS,
{ label: 'Up', value: 1 << 16 },
{ label: 'Down', value: 1 << 17 },
{ label: 'Left', value: 1 << 18 },
diff --git a/www/src/Data/Controllers.json b/www/src/Data/Controllers.json
index f45563443..3179300a3 100644
--- a/www/src/Data/Controllers.json
+++ b/www/src/Data/Controllers.json
@@ -29,6 +29,6 @@
"pin26": -10,
"pin27": -10,
"pin28": -10,
- "pin29": -10
+ "pin29": -5
}
}
diff --git a/www/src/Data/Pins.ts b/www/src/Data/Pins.ts
index 77ef0b49a..443c9fa42 100644
--- a/www/src/Data/Pins.ts
+++ b/www/src/Data/Pins.ts
@@ -1,6 +1,3 @@
-// Hide from select options / Disable select if returned from board
-export const NON_SELECTABLE_BUTTON_ACTIONS = [-5, 0];
-
// These could theoretically be created from enums.proto
export const BUTTON_ACTIONS = {
NONE: -10,
@@ -45,6 +42,7 @@ export const BUTTON_ACTIONS = {
BUTTON_PRESS_MACRO_4: 37,
BUTTON_PRESS_MACRO_5: 38,
BUTTON_PRESS_MACRO_6: 39,
+ CUSTOM_BUTTON_COMBO: 40,
} as const;
export const PIN_DIRECTIONS = {
diff --git a/www/src/Locales/en/Navigation.jsx b/www/src/Locales/en/Navigation.jsx
index db5011069..a7fb2f55b 100644
--- a/www/src/Locales/en/Navigation.jsx
+++ b/www/src/Locales/en/Navigation.jsx
@@ -12,6 +12,7 @@ export default {
'links-label': 'Links',
'macro-label': 'Macros Configuration',
'pin-mapping-label': 'Pin Mapping',
+ 'multi-mapping-label': 'Multi Mapping',
'peripheral-mapping-label': 'Peripheral Mapping',
'profile-settings-label': 'Profile Settings',
'reboot-label': 'Reboot',
diff --git a/www/src/Locales/en/PinMapping.jsx b/www/src/Locales/en/PinMapping.jsx
index e9c3002f6..83dce6983 100644
--- a/www/src/Locales/en/PinMapping.jsx
+++ b/www/src/Locales/en/PinMapping.jsx
@@ -1,6 +1,6 @@
export default {
'header-text': 'Pin Mapping',
- 'sub-header-text': `Here you can configure what pin has what action. If you're unsure what button is connect to what pin, try out the pin viewer.`,
+ 'sub-header-text': `If you're unsure what button is connect to what pin, try out the pin viewer.`,
'alert-text':
"Mapping buttons to pins that aren't connected or available can leave the device in non-functional state. To clear the invalid configuration go to the <2>Reset Settings2> page.",
'pin-viewer': 'Pin viewer',
@@ -59,5 +59,6 @@ export default {
BUTTON_PRESS_MACRO_4: 'Macro 4',
BUTTON_PRESS_MACRO_5: 'Macro 5',
BUTTON_PRESS_MACRO_6: 'Macro 6',
+ CUSTOM_BUTTON_COMBO: 'Assigned to multi mapping',
},
};
diff --git a/www/src/Pages/InputMacroAddonPage.tsx b/www/src/Pages/InputMacroAddonPage.tsx
index 25709f8ce..494149107 100644
--- a/www/src/Pages/InputMacroAddonPage.tsx
+++ b/www/src/Pages/InputMacroAddonPage.tsx
@@ -1,4 +1,4 @@
-import React, { useContext, useEffect, useMemo, useState } from 'react';
+import React, { useContext, useEffect, useState } from 'react';
import { AppContext } from '../Contexts/AppContext';
import {
Badge,
@@ -19,7 +19,11 @@ import omit from 'lodash/omit';
import Section from '../Components/Section';
import WebApi from '../Services/WebApi';
-import { getButtonLabels, BUTTONS, BUTTON_MASKS } from '../Data/Buttons';
+import {
+ getButtonLabels,
+ BUTTONS,
+ BUTTON_MASKS_OPTIONS,
+} from '../Data/Buttons';
const MACRO_TYPES = [
{ label: 'InputMacroAddon:input-macro-type.press', value: 1 },
@@ -181,7 +185,7 @@ const MacroInputComponent = (props) => {
- {BUTTON_MASKS.filter((mask) => buttonMask & mask.value).map(
+ {BUTTON_MASKS_OPTIONS.filter((mask) => buttonMask & mask.value).map(
(mask, i1) => (
{
isInvalid={errors?.buttonMask}
translation={t}
buttonLabelType={buttonLabelType}
- buttonMasks={BUTTON_MASKS}
+ buttonMasks={BUTTON_MASKS_OPTIONS}
/>
),
@@ -218,7 +222,7 @@ const MacroInputComponent = (props) => {
isInvalid={errors?.buttonMask}
translation={t}
buttonLabelType={buttonLabelType}
- buttonMasks={BUTTON_MASKS}
+ buttonMasks={BUTTON_MASKS_OPTIONS}
/>
@@ -433,7 +437,7 @@ const MacroComponent = (props) => {
}}
buttonLabelType={buttonLabelType}
translation={t}
- buttonMasks={BUTTON_MASKS.filter(
+ buttonMasks={BUTTON_MASKS_OPTIONS.filter(
(b, i) =>
macroList.find(
(m, macroIdx) =>
@@ -621,7 +625,7 @@ export default function MacrosPage() {
) : (
{`${
- BUTTON_MASKS.find(
+ BUTTON_MASKS_OPTIONS.find(
(b) =>
b.value == macro.macroTriggerButton,
).label
diff --git a/www/src/Pages/PinMapping.scss b/www/src/Pages/PinMapping.scss
new file mode 100644
index 000000000..7de724164
--- /dev/null
+++ b/www/src/Pages/PinMapping.scss
@@ -0,0 +1,6 @@
+.pin-grid {
+ display: grid;
+ grid-auto-flow: column;
+ grid-template-columns: 1fr 1fr;
+ grid-template-rows: repeat(15, auto);
+}
diff --git a/www/src/Pages/PinMapping.tsx b/www/src/Pages/PinMapping.tsx
index bf6d0114c..c4530b5d4 100644
--- a/www/src/Pages/PinMapping.tsx
+++ b/www/src/Pages/PinMapping.tsx
@@ -1,16 +1,9 @@
-import React, {
- useCallback,
- useContext,
- useEffect,
- useMemo,
- useState,
-} from 'react';
+import React, { useCallback, useContext, useEffect, useState } from 'react';
import { NavLink } from 'react-router-dom';
-import { Alert, Button, Form, Tab, Tabs } from 'react-bootstrap';
+import { Alert, Button, Col, Form, Nav, Row, Tab } from 'react-bootstrap';
import { Trans, useTranslation } from 'react-i18next';
import invert from 'lodash/invert';
import omit from 'lodash/omit';
-import zip from 'lodash/zip';
import { AppContext } from '../Contexts/AppContext';
import usePinStore from '../Store/usePinStore';
@@ -20,49 +13,191 @@ import Section from '../Components/Section';
import CustomSelect from '../Components/CustomSelect';
import CaptureButton from '../Components/CaptureButton';
-import { getButtonLabels } from '../Data/Buttons';
-import {
- BUTTON_ACTIONS,
- NON_SELECTABLE_BUTTON_ACTIONS,
- PinActionValues,
-} from '../Data/Pins';
+import { BUTTON_MASKS, DPAD_MASKS, getButtonLabels } from '../Data/Buttons';
+import { BUTTON_ACTIONS, PinActionValues } from '../Data/Pins';
+import './PinMapping.scss';
+import { MultiValue, SingleValue } from 'react-select';
-type PinCell = [string, PinActionValues];
-type PinRow = [PinCell, PinCell];
-type PinList = [PinRow];
+type OptionType = {
+ label: string;
+ value: PinActionValues;
+ type: string;
+ customButtonMask: number;
+ customDpadMask: number;
+};
+
+type ProfilePinSectionType = {
+ title: string;
+ profilePins: { [key: string]: PinActionValues };
+ profileIndex: number;
+};
+
+const disabledOptions = [
+ BUTTON_ACTIONS.RESERVED,
+ BUTTON_ACTIONS.ASSIGNED_TO_ADDON,
+ BUTTON_ACTIONS.BUTTON_PRESS_TURBO,
+ BUTTON_ACTIONS.BUTTON_PRESS_MACRO,
+];
+
+const getMask = (maskArr, key) =>
+ maskArr.find(
+ ({ label }) => label?.toUpperCase() === key.split('BUTTON_PRESS_')?.pop(),
+ );
-const isNonSelectable = (value) =>
- NON_SELECTABLE_BUTTON_ACTIONS.includes(value);
+const isNonSelectable = (action) =>
+ [
+ BUTTON_ACTIONS.NONE,
+ BUTTON_ACTIONS.CUSTOM_BUTTON_COMBO,
+ ...disabledOptions,
+ ].includes(action);
+
+const isDisabled = (action) => disabledOptions.includes(action);
const options = Object.entries(BUTTON_ACTIONS)
.filter(([, value]) => !isNonSelectable(value))
- .map(([key, value]) => ({
- label: key,
- value,
- }));
-
-const getOption = (actionId) => ({
- label: invert(BUTTON_ACTIONS)[actionId],
- value: actionId,
-});
-
-type PinsFormTypes = {
- savePins: () => void;
- pins: { [key: string]: PinActionValues };
- setPinAction: (pin: string, action: PinActionValues) => void;
- onCopy?: () => void;
+ .map(([key, value]) => {
+ const buttonMask = getMask(BUTTON_MASKS, key);
+ const dpadMask = getMask(DPAD_MASKS, key);
+
+ return {
+ label: key,
+ value,
+ type: buttonMask
+ ? 'customButtonMask'
+ : dpadMask
+ ? 'customDpadMask'
+ : 'action',
+ customButtonMask: buttonMask?.value || 0,
+ customDpadMask: dpadMask?.value || 0,
+ };
+ });
+
+const groupedOptions = [
+ {
+ label: 'Buttons',
+ options: options.filter(({ type }) => type !== 'action'),
+ },
+ {
+ label: 'Actions',
+ options: options.filter(({ type }) => type === 'action'),
+ },
+];
+
+const getMultiValue = (pinData) => {
+ if (pinData.action === BUTTON_ACTIONS.NONE) return;
+ if (isDisabled(pinData.action)) {
+ const actionKey = invert(BUTTON_ACTIONS)[pinData.action];
+ return [{ label: actionKey, ...pinData }];
+ }
+
+ return pinData.action === BUTTON_ACTIONS.CUSTOM_BUTTON_COMBO
+ ? options.filter(
+ ({ type, customButtonMask, customDpadMask }) =>
+ (pinData.customButtonMask & customButtonMask &&
+ type === 'customButtonMask') ||
+ (pinData.customDpadMask & customDpadMask &&
+ type === 'customDpadMask'),
+ )
+ : options.filter((option) => option.value === pinData.action);
};
-const PinsForm = ({ savePins, pins, setPinAction, onCopy }: PinsFormTypes) => {
+const getSingleValue = (pinData) => {
+ const actionKey = invert(BUTTON_ACTIONS)[pinData];
+ return { label: actionKey, value: pinData };
+};
+
+const PinMappingWarning = () => {
+ const { t } = useTranslation('');
+ return (
+
+
+ Mapping buttons to pins that aren't connected or available can
+ leave the device in non-functional state. To clear the invalid
+ configuration go to the{' '}
+ Reset Settings page.
+
+
+
+ {t(`PinMapping:profile-pins-warning`)}
+
+ );
+};
+const BasePinSection = () => {
+ const { pins, savePins, setPin } = usePinStore();
const { buttonLabels, updateUsedPins } = useContext(AppContext);
+ const { t } = useTranslation('');
const [saveMessage, setSaveMessage] = useState('');
const { buttonLabelType, swapTpShareLabels } = buttonLabels;
const CURRENT_BUTTONS = getButtonLabels(buttonLabelType, swapTpShareLabels);
const buttonNames = omit(CURRENT_BUTTONS, ['label', 'value']);
- const { t } = useTranslation('');
- const handleSubmit = async (e) => {
+ const onChange = useCallback(
+ (pin: string) =>
+ (selected: MultiValue | SingleValue) => {
+ // Handle clearing
+ if (!selected || (Array.isArray(selected) && !selected.length)) {
+ setPin(pin, {
+ action: BUTTON_ACTIONS.NONE,
+ customButtonMask: 0,
+ customDpadMask: 0,
+ });
+ } else if (Array.isArray(selected) && selected.length > 1) {
+ const lastSelected = selected[selected.length - 1];
+ // Revert to single option if choosing action type
+ if (lastSelected.type === 'action') {
+ setPin(pin, {
+ action: lastSelected.value,
+ customButtonMask: 0,
+ customDpadMask: 0,
+ });
+ } else {
+ setPin(
+ pin,
+ selected.reduce(
+ (masks, option) => ({
+ ...masks,
+ customButtonMask:
+ option.type === 'customButtonMask'
+ ? masks.customButtonMask ^ option.customButtonMask
+ : masks.customButtonMask,
+ customDpadMask:
+ option.type === 'customDpadMask'
+ ? masks.customDpadMask ^ option.customDpadMask
+ : masks.customDpadMask,
+ }),
+ {
+ action: BUTTON_ACTIONS.CUSTOM_BUTTON_COMBO,
+ customButtonMask: 0,
+ customDpadMask: 0,
+ },
+ ),
+ );
+ }
+ } else {
+ setPin(pin, {
+ action: selected[0].value,
+ customButtonMask: 0,
+ customDpadMask: 0,
+ });
+ }
+ },
+ [],
+ );
+
+ const getOptionLabel = useCallback(
+ (option: OptionType) => {
+ const labelKey = option.label?.split('BUTTON_PRESS_')?.pop();
+ // Need to fallback as some button actions are not part of button names
+ return (
+ (labelKey && buttonNames[labelKey]) ||
+ t(`PinMapping:actions.${option.label}`)
+ );
+ },
+ [buttonNames],
+ );
+
+ const handleSubmit = useCallback(async (e) => {
e.preventDefault();
e.stopPropagation();
try {
@@ -72,59 +207,144 @@ const PinsForm = ({ savePins, pins, setPinAction, onCopy }: PinsFormTypes) => {
} catch (error) {
setSaveMessage(t('Common:saved-error-message'));
}
- };
-
- const pinList = useMemo(() => {
- const pinArray = Object.entries(pins);
- return zip(
- pinArray.slice(0, pinArray.length / 2),
- pinArray.slice(pinArray.length / 2, pinArray.length),
- );
- }, [pins]);
-
- const createCell = useCallback(
- ([pin, pinAction]: PinCell) => (
-
-
-
-
- {
- const labelKey = option.label.split('BUTTON_PRESS_').pop();
- // Need to fallback as some button actions are not part of button names
- return (
- (labelKey && buttonNames[labelKey]) ||
- t(`PinMapping:actions.${option.label}`)
- );
- }}
- onChange={(change) =>
- setPinAction(
- pin,
- change?.value === undefined ? -10 : change.value, // On clear set to -10
- )
- }
- />
-
- ),
+ }, []);
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
+const ProfilePinSection = ({
+ title,
+ profilePins,
+ profileIndex,
+}: ProfilePinSectionType) => {
+ const { saveProfiles, setProfileAction } = useProfilesStore();
+ const { buttonLabels, updateUsedPins } = useContext(AppContext);
+ const { t } = useTranslation('');
+ const [saveMessage, setSaveMessage] = useState('');
+
+ const { buttonLabelType, swapTpShareLabels } = buttonLabels;
+ const CURRENT_BUTTONS = getButtonLabels(buttonLabelType, swapTpShareLabels);
+ const buttonNames = omit(CURRENT_BUTTONS, ['label', 'value']);
+
+ const onChange = useCallback(
+ (pin: string, profileIndex: number) =>
+ (selected: SingleValue) => {
+ setProfileAction(
+ profileIndex,
+ pin,
+ selected?.value === undefined ? BUTTON_ACTIONS.NONE : selected.value,
+ );
+ },
+ [],
+ );
+
+ const getOptionLabel = useCallback(
+ (option: OptionType) => {
+ const labelKey = option.label?.split('BUTTON_PRESS_')?.pop();
+ // Need to fallback as some button actions are not part of button names
+ return (
+ (labelKey && buttonNames[labelKey]) ||
+ t(`PinMapping:actions.${option.label}`)
+ );
+ },
[buttonNames],
);
+ const handleSubmit = useCallback(async (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ try {
+ await saveProfiles();
+ updateUsedPins();
+ setSaveMessage(t('Common:saved-success-message'));
+ } catch (error) {
+ setSaveMessage(t('Common:saved-error-message'));
+ }
+ }, []);
+
return (
-
+
+ {saveMessage && {saveMessage}}
+
+
+ >
);
};
-export default function PinMappingPage() {
- const { fetchPins, pins, savePins, setPinAction } = usePinStore();
- const {
- fetchProfiles,
- profiles,
- saveProfiles,
- setProfileAction,
- setProfile,
- } = useProfilesStore();
-
+export default function PinMapping() {
+ const { fetchPins } = usePinStore();
+ const { fetchProfiles, profiles } = useProfilesStore();
const [pressedPin, setPressedPin] = useState(null);
-
const { t } = useTranslation('');
useEffect(() => {
@@ -176,67 +373,60 @@ export default function PinMappingPage() {
fetchProfiles();
}, []);
- const saveAll = useCallback(() => {
- savePins();
- saveProfiles();
- }, [savePins, saveProfiles]);
-
return (
- <>
-
-
- Mapping buttons to pins that aren't connected or available can
- leave the device in non-functional state. To clear the invalid
- configuration go to the{' '}
- Reset Settings page.
-
-
-
- {t(`PinMapping:profile-pins-warning`)}
-
-
- {t('PinMapping:sub-header-text')}
-
- setPressedPin(pin)}
- />
-
- {pressedPin !== null && (
-
- {t('PinMapping:pin-pressed', { pressedPin })}
-
- )}
-
-
-
-
+
+
+
+
+ {t('PinMapping:sub-header-text')}
+
+ setPressedPin(pin)}
/>
-
- {profiles.map((profilePins, profileIndex) => (
-
- {
- setProfileAction(profileIndex, pin, action);
- }}
- onCopy={() => {
- setProfile(profileIndex, pins);
- }}
- />
-
- ))}
-
-
- >
+
+ {pressedPin !== null && (
+
+ {t('PinMapping:pin-pressed', { pressedPin })}
+
+ )}
+
+
+
+
+
+
+
+ {profiles.map((profilePins, profileIndex) => (
+
+
+
+ ))}
+
+
+
+
);
}
diff --git a/www/src/Pages/SettingsPage.jsx b/www/src/Pages/SettingsPage.jsx
index 04fcb66d4..7819da86d 100644
--- a/www/src/Pages/SettingsPage.jsx
+++ b/www/src/Pages/SettingsPage.jsx
@@ -11,7 +11,7 @@ import { AppContext } from '../Contexts/AppContext';
import KeyboardMapper, { validateMappings } from '../Components/KeyboardMapper';
import Section from '../Components/Section';
import WebApi, { baseButtonMappings } from '../Services/WebApi';
-import { BUTTON_MASKS, getButtonLabels } from '../Data/Buttons';
+import { BUTTON_MASKS_OPTIONS, getButtonLabels } from '../Data/Buttons';
import './SettingsPage.scss';
@@ -1350,7 +1350,7 @@ export default function SettingsPage() {
+
- {BUTTON_MASKS.map((mask) =>
+ {BUTTON_MASKS_OPTIONS.map((mask) =>
values[o] &&
values[o]?.buttonsMask & mask.value ? (
<>
@@ -1377,7 +1377,7 @@ export default function SettingsPage() {
);
}}
>
- {BUTTON_MASKS.map((o, i2) => (
+ {BUTTON_MASKS_OPTIONS.map((o, i2) => (
|