Add behavior picker component #33

Merged
MrKBear merged 3 commits from dev-mrkbear into master 2022-04-02 21:01:51 +08:00
9 changed files with 234 additions and 31 deletions
Showing only changes of commit 555648dbb0 - Show all commits

View File

@ -39,8 +39,11 @@ div.behavior-picker-list {
i.view-icon {
display: none;
}
}
i.view-icon:hover {
div.behavior-picker-line-icon-view:hover {
i.view-icon {
color: $lt-green;
}
}
@ -57,16 +60,28 @@ div.behavior-picker-list {
}
}
div.behavior-picker-title.is-deleted {
div {
text-decoration: line-through;
opacity: 0.6;
}
}
div.behavior-picker-title.behavior-add-line {
width: calc(100% - 30px);
}
div.behavior-picker-line-delete-view {
width: 30px;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
i:hover {
color: $lt-red;
}
div.behavior-picker-line-delete-view:hover i {
color: $lt-red;
}
}

View File

@ -1,77 +1,190 @@
import { DetailsList } from "@Component/DetailsList/DetailsList";
import { Component, ReactNode } from "react";
import { Component, ReactNode, createRef } from "react";
import { Behavior } from "@Model/Behavior";
import { Icon } from "@fluentui/react";
import { useSettingWithEvent, IMixinSettingProps } from "@Context/Setting";
import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status";
import { Localization } from "@Component/Localization/Localization";
import { PickerList } from "@Component/PickerList/PickerList";
import "./BehaviorPicker.scss";
interface IBehaviorPickerProps {
behavior: Behavior[];
focusBehavior?: Behavior;
click?: (behavior: Behavior) => void;
delete?: (behavior: Behavior) => void;
action?: (behavior: Behavior) => void;
add?: () => void;
add?: (behavior: Behavior) => void;
}
interface IBehaviorPickerState {
isPickerListOpen: boolean;
}
@useStatusWithEvent("behaviorChange")
@useSettingWithEvent("language")
class BehaviorPicker extends Component<IBehaviorPickerProps & IMixinSettingProps> {
class BehaviorPicker extends Component<IBehaviorPickerProps & IMixinSettingProps & IMixinStatusProps> {
public state = {
isPickerListOpen: false
}
private isInnerClick: boolean = false;
private clickLineRef = createRef<HTMLDivElement>();
private getData() {
let data: Array<{key: string, behavior: Behavior | undefined}> = [];
let data: Array<{select: boolean, key: string, behavior: Behavior | undefined}> = [];
for (let i = 0; i < this.props.behavior.length; i++) {
data.push({
key: this.props.behavior[i].id,
behavior: this.props.behavior[i]
behavior: this.props.behavior[i],
select: this.props.behavior[i].id === this.props.focusBehavior?.id
})
}
data.push({
key: "@@AddButton_List_Key",
behavior: undefined
behavior: undefined,
select: false
})
return data;
}
private renderLine = (behavior?: Behavior): ReactNode => {
if (behavior) {
const titleClassList: string[] = ["behavior-picker-title"];
if (this.props.setting) {
titleClassList.push(this.props.setting.language);
}
if (behavior.isDeleted()) {
titleClassList.push("is-deleted");
}
return <>
<div className="behavior-picker-line-color-view">
<div style={{ borderLeft: `10px solid ${behavior.color}` }}/>
</div>
<div className="behavior-picker-line-icon-view">
<Icon iconName={behavior.iconName} className="behavior-icon"/>
<Icon iconName="EditCreate" className="view-icon"/>
<div
className="behavior-picker-line-icon-view"
onClick={() => {
this.isInnerClick = true;
this.props.action && this.props.action(behavior);
}}
>
{
behavior.isDeleted() ?
<Icon iconName={behavior.iconName}/>:
<>
<Icon iconName={behavior.iconName} className="behavior-icon"/>
<Icon iconName="EditCreate" className="view-icon"/>
</>
}
</div>
<div className={`behavior-picker-title ${this.props.setting?.language}`}>
<div className={titleClassList.join(" ")}>
<div>{behavior.name}</div>
</div>
<div className="behavior-picker-line-delete-view">
<div
className="behavior-picker-line-delete-view"
onClick={() => {
this.isInnerClick = true;
this.props.delete && this.props.delete(behavior);
}}
>
<Icon iconName="Delete"/>
</div>
</>;
} else {
const openPicker = () => {
this.isInnerClick = true;
this.setState({
isPickerListOpen: true
});
}
return <>
<div className="behavior-picker-line-icon-view">
<div
className="behavior-picker-line-icon-view"
onClick={openPicker}
>
<Icon iconName="Add" className="add-icon"/>
</div>
<div className={`behavior-picker-title`}>
<div
className="behavior-picker-title behavior-add-line"
onClick={openPicker}
ref={this.clickLineRef}
>
<Localization i18nKey="Behavior.Picker.Add.Button"/>
</div>
</>;
}
}
public render(): ReactNode {
return <DetailsList
hideCheckBox
className="behavior-picker-list"
items={this.getData()}
columns={[{
className: "behavior-picker-line",
key: "behavior",
render: this.renderLine
}]}
private getAllData = (): Behavior[] => {
if (this.props.status) {
let res: Behavior[] = [];
for (let i = 0; i < this.props.status.model.behaviorPool.length; i++) {
let isAdded = false;
for (let j = 0; j < this.props.behavior.length; j++) {
if (this.props.status.model.behaviorPool[i].id === this.props.behavior[j].id) {
isAdded = true;
break;
}
}
if (!isAdded) {
res.push(this.props.status.model.behaviorPool[i]);
}
}
return res;
} else {
return [];
}
}
private renderPickerList(): ReactNode {
return <PickerList
objectList={this.getAllData()}
noData="Behavior.Picker.Add.Nodata"
target={this.clickLineRef}
clickObjectItems={((item) => {
if (item instanceof Behavior && this.props.add) {
this.props.add(item);
}
this.setState({
isPickerListOpen: false
})
})}
dismiss={() => {
this.setState({
isPickerListOpen: false
});
}}
/>
}
public render(): ReactNode {
return <>
<DetailsList
hideCheckBox
className="behavior-picker-list"
items={this.getData()}
clickLine={(item) => {
if (!this.isInnerClick && this.props.click && item.behavior) {
this.props.click(item.behavior);
}
this.isInnerClick = false;
}}
columns={[{
className: "behavior-picker-line",
key: "behavior",
render: this.renderLine
}]}
/>
{this.state.isPickerListOpen ? this.renderPickerList() : null}
</>
}
}
export { BehaviorPicker };

View File

@ -8,6 +8,7 @@ import { PickerList, IDisplayItem, getObjectDisplayInfo, IDisplayInfo } from "..
import { Localization } from "@Component/Localization/Localization";
import { Icon } from "@fluentui/react";
import { CtrlObject } from "@Model/CtrlObject";
import { Behavior } from "@Model/Behavior";
import "./ObjectPicker.scss";
type IObjectType = Label | Group | Range | CtrlObject;
@ -80,6 +81,7 @@ class ObjectPicker extends Component<IObjectPickerProps & IMixinStatusProps, IOb
target={this.pickerTarget}
objectList={this.getAllOption()}
clickObjectItems={((item) => {
if (item instanceof Behavior) return;
if (this.props.valueChange) {
this.props.valueChange(item);
}

View File

@ -1,5 +1,6 @@
import { AllI18nKeys, Localization } from "@Component/Localization/Localization";
import { Callout, DirectionalHint, Icon } from "@fluentui/react";
import { Behavior } from "@Model/Behavior";
import { CtrlObject } from "@Model/CtrlObject";
import { Group } from "@Model/Group";
import { Label } from "@Model/Label";
@ -7,7 +8,7 @@ import { Range } from "@Model/Range";
import { Component, ReactNode, RefObject } from "react";
import "./PickerList.scss";
type IPickerListItem = CtrlObject | Label | Range | Group;
type IPickerListItem = CtrlObject | Label | Range | Group | Behavior;
interface IDisplayInfo {
color: string;
icon: string;
@ -75,6 +76,14 @@ function getObjectDisplayInfo(item?: IPickerListItem): IDisplayInfo {
}
}
if (item instanceof Behavior) {
color = item.color;
icon = item.iconName;
name = item.name;
internal = false;
allLabel = false;
}
if (Array.isArray(color)) {
color = `rgb(${color[0]},${color[1]},${color[2]})`;
}
@ -159,7 +168,7 @@ class PickerList extends Component<IPickerListProps> {
return <Callout
onDismiss={this.props.dismiss}
target={this.props.target}
directionalHint={DirectionalHint.topAutoEdge}
directionalHint={DirectionalHint.topCenter}
>
<div className="picker-list-root">
{this.props.objectList ? this.props.objectList.map((item) => {

View File

@ -38,6 +38,7 @@ interface IStatusEvent {
objectChange: void;
rangeLabelChange: void;
groupLabelChange: void;
groupBehaviorChange: void;
labelChange: void;
rangeAttrChange: void;
labelAttrChange: void;
@ -192,6 +193,22 @@ class Status extends Emitter<IStatusEvent> {
}
}
public addGroupBehavior(id: ObjectID, val: Behavior) {
const group = this.model.getObjectById(id);
if (group && group instanceof Group) {
group.addBehavior(val);
this.emit("groupBehaviorChange");
}
}
public deleteGroupBehavior(id: ObjectID, val: Behavior) {
const group = this.model.getObjectById(id);
if (group && group instanceof Group) {
group.deleteBehavior(val);
this.emit("groupBehaviorChange");
}
}
public addGroupLabel(id: ObjectID, val: Label) {
const group = this.model.getObjectById(id);
if (group && group instanceof Group) {

View File

@ -31,6 +31,7 @@ const EN_US = {
"Object.List.No.Data": "There are no objects in the model, click the button to create it",
"Object.Picker.List.No.Data": "There is no model in the model for this option",
"Behavior.Picker.Add.Button": "Click here to assign behavior to this group",
"Behavior.Picker.Add.Nodata": "There is no behavior that can be specified",
"Panel.Title.Notfound": "{id}",
"Panel.Info.Notfound": "This panel with id {id} can not found!",
"Panel.Title.Render.View": "Live preview",

View File

@ -31,6 +31,7 @@ const ZH_CN = {
"Object.List.No.Data": "模型中没有任何对象,点击按钮以创建",
"Object.Picker.List.No.Data": "模型中没有合适此选项的模型",
"Behavior.Picker.Add.Button": "点击此处以赋予行为到此群",
"Behavior.Picker.Add.Nodata": "没有可以被指定的行为",
"Panel.Title.Notfound": "{id}",
"Panel.Info.Notfound": "这个编号为 {id} 的面板无法找到!",
"Panel.Title.Render.View": "实时预览",

View File

@ -315,8 +315,12 @@ class Group extends CtrlObject {
public addBehavior(behavior: Behavior | Behavior[]): this {
if (Array.isArray(behavior)) {
this.behaviors = this.behaviors.concat(behavior);
for (let i = 0; i < behavior.length; i++) {
behavior[i].mount(this, this.model);
}
} else {
this.behaviors.push(behavior);
behavior.mount(this, this.model);
}
// 按照优先级
@ -326,6 +330,27 @@ class Group extends CtrlObject {
return this;
}
/**
*
* @param behavior
*/
public deleteBehavior(behavior: Behavior): this {
let deleteIndex = -1;
for (let i = 0; i < this.behaviors.length; i++) {
if (this.behaviors[i].id === behavior.id) {
deleteIndex = i;
}
}
if (deleteIndex >= 0) {
this.behaviors[deleteIndex].unmount(this, this.model);
this.behaviors.splice(deleteIndex, 1);
}
return this;
}
/**
*
* @param
@ -333,7 +358,11 @@ class Group extends CtrlObject {
public runner(t: number, effectType: "finalEffect" | "effect" | "afterEffect" ): void {
this.individuals.forEach((individual) => {
for(let j = 0; j < this.behaviors.length; j++) {
this.behaviors[j][effectType](individual, this, this.model, t);
if (this.behaviors[j].isDeleted()) {
continue;
} else {
this.behaviors[j][effectType](individual, this, this.model, t);
}
}
});
}

View File

@ -25,7 +25,10 @@ const allOption: IDisplayItem[] = [
{nameKey: "Common.Attr.Key.Generation.Mod.Range", key: GenMod.Range}
];
@useStatusWithEvent("groupAttrChange", "groupLabelChange", "focusObjectChange")
@useStatusWithEvent(
"groupAttrChange", "groupLabelChange", "focusObjectChange",
"focusBehaviorChange", "behaviorChange", "groupBehaviorChange"
)
class GroupDetails extends Component<IGroupDetailsProps & IMixinStatusProps> {
private renderFrom(group: Group) {
@ -113,6 +116,19 @@ class GroupDetails extends Component<IGroupDetailsProps & IMixinStatusProps> {
<BehaviorPicker
behavior={group.behaviors}
focusBehavior={this.props.status?.focusBehavior}
click={(behavior) => {
this.props.status?.setBehaviorObject(behavior);
}}
action={(behavior) => {
this.props.status?.setBehaviorObject(behavior);
}}
delete={(behavior) => {
this.props.status?.deleteGroupBehavior(group.id, behavior);
}}
add={(behavior) => {
this.props.status?.addGroupBehavior(group.id, behavior);
}}
/>
<Message i18nKey="Common.Attr.Title.Individual.Generation" isTitle/>