Skip to content

Commit

Permalink
Merge pull request #1399 from pau-tomas/feat/flag_rename
Browse files Browse the repository at this point in the history
Allow to rename variable flags
  • Loading branch information
chrismaltby authored May 13, 2024
2 parents 621ce9d + 40ddb5c commit 758f8be
Show file tree
Hide file tree
Showing 14 changed files with 694 additions and 530 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add `Script Lock` and `Script Unlock` events allowing pausing other scripts and scene updates until the script is completed or unlocked
- Add `Build Options` to "Settings" section with option to toggle if "Build Log" should be opened automatically on warnings
- Add `Show Navigator` button to World toolbar if navigator is closed
- Add ability to rename flags in Variable Flags Add/Clear/Set events [@pau-tomas](https://github.com/pau-tomas)

### Changed

Expand Down
63 changes: 63 additions & 0 deletions src/components/forms/FlagSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { FC } from "react";
import l10n from "shared/lib/lang/l10n";
import { variableSelectors } from "store/features/entities/entitiesState";
import { useAppSelector } from "store/hooks";
import { Select, SelectCommonProps } from "ui/form/Select";

interface FlagSelectProps extends SelectCommonProps {
name: string;
value?: string;
variableId: string;
entityId: string;
onChange?: (newId: string) => void;
}

export const FlagSelect: FC<FlagSelectProps> = ({
value,
variableId,
entityId,
onChange,
...selectProps
}) => {
const variableIsLocal = variableId && variableId.startsWith("L");

const namedVariable = useAppSelector((state) => {
let id = variableId;
if (variableIsLocal) {
id = `${entityId}__${variableId}`;
}
return variableSelectors.selectById(state, id);
});

const flagOptions = Array(16)
.fill(0)
.map((_, i) => {
let namedLabel = l10n("FIELD_FLAG_N", { n: i + 1 });
if (namedVariable?.flags && namedVariable?.flags[`flag${i + 1}`]) {
namedLabel = namedVariable?.flags[`flag${i + 1}`];
}
return {
label: namedLabel,
value: `${i}`,
};
});

const currentValue =
flagOptions.find((o) => (value ? o.value === value : o.value === value)) ||
flagOptions[0];

const onFieldChange = (newOption: { value: string }) => {
if (onChange) {
onChange(newOption.value);
}
};

return (
<Select
value={currentValue}
options={flagOptions}
onChange={onFieldChange}
{...selectProps}
/>
);
};
1 change: 1 addition & 0 deletions src/components/script/ScriptEventFormField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ const ScriptEventFormField = memo(
label &&
field.type !== "checkbox" &&
field.type !== "group" &&
field.type !== "flag" &&
!field.hideLabel
? labelWithUnits
: ""
Expand Down
27 changes: 27 additions & 0 deletions src/components/script/ScriptEventFormInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ import { setDefault } from "shared/lib/helpers/setDefault";
import { TilesetSelect } from "components/forms/TilesetSelect";
import ValueSelect from "components/forms/ValueSelect";
import { isScriptValue } from "shared/lib/scriptValue/types";
import { FlagField } from "ui/form/FlagField";
import { FlagSelect } from "components/forms/FlagSelect";

interface ScriptEventFormInputProps {
id: string;
Expand Down Expand Up @@ -276,6 +278,31 @@ const ScriptEventFormInput = ({
onChange={onChangeCheckboxField}
/>
);
} else if (type === "flag") {
return (
<FlagField
name={id}
bit={field.key ?? "flag1"}
defaultLabel={String(field.checkboxLabel || field.label)}
title={field.description}
variableId={argValue(args.variable) as string}
checked={
typeof value === "boolean" ? value : Boolean(defaultValue || false)
}
entityId={entityId}
onChange={onChangeCheckboxField}
/>
);
} else if (type === "selectFlags") {
return (
<FlagSelect
name={id}
variableId={argValue(args.variable) as string}
entityId={entityId}
value={String(value ?? defaultValue)}
onChange={onChangeField}
></FlagSelect>
);
} else if (type === "select") {
const options = (field.options || []).map(([value, label]) => ({
value,
Expand Down
246 changes: 246 additions & 0 deletions src/components/ui/form/FlagField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import React, { FC, useState } from "react";
import styled from "styled-components";
import { Checkbox } from "./Checkbox";
import { Label } from "./Label";
import { CheckIcon, PencilIcon } from "ui/icons/Icons";
import l10n from "shared/lib/lang/l10n";
import { useAppDispatch, useAppSelector } from "store/hooks";
import { Input } from "ui/form/Input";
import entitiesActions from "store/features/entities/entitiesActions";
import { variableSelectors } from "store/features/entities/entitiesState";

export interface FlagFieldFieldProps {
readonly name: string;
readonly bit: string;
readonly variableId: string;
readonly entityId: string;
readonly defaultLabel?: string;
readonly title?: string;
readonly checked?: boolean;
readonly onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
}

const Wrapper = styled.div`
position: relative;
display: flex;
align-items: center;
width: 100%;
height: 28px;
${Label} {
margin-left: 5px;
margin-bottom: 0px;
margin-top: -1px;
}
`;

const VariableRenameInput = styled(Input)`
&&&& {
padding-right: 32px;
height: 28px;
}
`;

const VariableRenameButton = styled.button`
position: absolute;
top: 3px;
right: 3px;
width: 22px;
height: 22px;
border: 0;
border-radius: ${(props) => Math.max(0, props.theme.borderRadius - 1)}px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
line-height: 10px;
font-size: 12px;
font-weight: bold;
opacity: 0;
transition: opacity 0.2s ease-in-out;
background: transparent; // ${(props) => props.theme.colors.input.background};
border-color: ${(props) => props.theme.colors.input.background};
${Wrapper}:hover & {
opacity: 1;
}
:focus {
opacity: 1;
}
:hover {
background: rgba(128, 128, 128, 0.3);
}
:active {
background: rgba(128, 128, 128, 0.4);
}
svg {
width: 12px;
height: 12px;
fill: #666;
}
`;

const VariableRenameCompleteButton = styled.button`
position: absolute;
top: 3px;
right: 3px;
width: 22px;
height: 22px;
border: 0;
border-radius: ${(props) => Math.max(0, props.theme.borderRadius - 1)}px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
line-height: 10px;
font-size: 12px;
font-weight: bold;
background: transparent;
border-color: transparent;
z-index: 10001;
:hover {
background: rgba(128, 128, 128, 0.3);
}
:active {
background: rgba(128, 128, 128, 0.4);
}
svg {
width: 12px;
height: 12px;
fill: #333;
}
`;

export const FlagField: FC<FlagFieldFieldProps> = ({
name,
variableId,
bit,
defaultLabel,
title,
checked,
entityId,
onChange,
}) => {
const variableIsLocal = variableId && variableId.startsWith("L");
const variableIsTemp = variableId && variableId.startsWith("T");
const variableIsParam = variableId && variableId.startsWith("V");

const namedVariable = useAppSelector((state) => {
let id = variableId;
if (variableIsLocal) {
id = `${entityId}__${variableId}`;
}
return variableSelectors.selectById(state, id);
});

const canRename = !variableIsTemp && !variableIsParam;

const label =
(namedVariable?.flags && namedVariable?.flags[bit]) ?? defaultLabel;

const [renameVisible, setRenameVisible] = useState(false);
const [editValue, setEditValue] = useState(label);

const dispatch = useAppDispatch();

const onRenameStart = () => {
setEditValue(label);
setRenameVisible(true);
};

const onRenameFocus = (e: React.FocusEvent<HTMLInputElement>) => {
e.currentTarget.select();
};

const onRename = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditValue(e.currentTarget.value);
};

const onRenameKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
onRenameFinish();
} else if (e.key === "Escape") {
setRenameVisible(false);
}
};

const onRenameFinish = () => {
const flags = namedVariable?.flags ?? {};
const newFlags: Record<string, string> = { ...flags };

if (editValue) {
newFlags[bit] = editValue ?? "";
} else {
delete newFlags[bit];
}

if (variableIsLocal) {
dispatch(
entitiesActions.renameVariableFlags({
variableId: `${entityId}__${variableId}`,
flags: newFlags,
})
);
} else {
dispatch(
entitiesActions.renameVariableFlags({
variableId: variableId || "0",
flags: newFlags,
})
);
}
setRenameVisible(false);
};

return (
<Wrapper>
{renameVisible ? (
<VariableRenameInput
key={variableId}
value={editValue}
onChange={onRename}
onKeyDown={onRenameKeyDown}
onFocus={onRenameFocus}
onBlur={onRenameFinish}
autoFocus
/>
) : (
<>
<Checkbox
id={name}
name={name}
checked={checked}
onChange={onChange}
/>
{label && (
<Label htmlFor={name} title={title}>
{label}
</Label>
)}
</>
)}
<>
{canRename &&
(renameVisible ? (
<VariableRenameCompleteButton
onClick={onRenameFinish}
title={l10n("FIELD_RENAME")}
>
<CheckIcon />
</VariableRenameCompleteButton>
) : (
<VariableRenameButton
onClick={onRenameStart}
title={l10n("FIELD_RENAME")}
>
<PencilIcon />
</VariableRenameButton>
))}
</>
</Wrapper>
);
};
2 changes: 1 addition & 1 deletion src/lib/compiler/scriptBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4586,7 +4586,7 @@ extern void __mute_mask_${symbol};
name = tempVariableName(num);
} else {
const num = toVariableNumber(variable || "0");
name = namedVariable?.name ?? globalVariableDefaultName(num);
name = namedVariable?.name || globalVariableDefaultName(num);
}

const alias = "VAR_" + toASMVar(name);
Expand Down
Loading

0 comments on commit 758f8be

Please sign in to comment.