Merge pull request 'Add behavior picker component' (#33) from dev-mrkbear into master

Reviewed-on: http://git.mrkbear.com/MrKBear/living-together/pulls/33
This commit is contained in:
MrKBear 2022-04-02 21:01:50 +08:00
commit fffea60cee
9 changed files with 234 additions and 31 deletions

View File

@ -39,8 +39,11 @@ div.behavior-picker-list {
i.view-icon { i.view-icon {
display: none; display: none;
} }
}
i.view-icon:hover { div.behavior-picker-line-icon-view:hover {
i.view-icon {
color: $lt-green; 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 { div.behavior-picker-line-delete-view {
width: 30px; width: 30px;
height: 100%; height: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
i:hover {
color: $lt-red;
} }
div.behavior-picker-line-delete-view:hover i {
color: $lt-red;
} }
} }

View File

@ -1,76 +1,189 @@
import { DetailsList } from "@Component/DetailsList/DetailsList"; import { DetailsList } from "@Component/DetailsList/DetailsList";
import { Component, ReactNode } from "react"; import { Component, ReactNode, createRef } from "react";
import { Behavior } from "@Model/Behavior"; import { Behavior } from "@Model/Behavior";
import { Icon } from "@fluentui/react"; import { Icon } from "@fluentui/react";
import { useSettingWithEvent, IMixinSettingProps } from "@Context/Setting"; import { useSettingWithEvent, IMixinSettingProps } from "@Context/Setting";
import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status";
import { Localization } from "@Component/Localization/Localization"; import { Localization } from "@Component/Localization/Localization";
import { PickerList } from "@Component/PickerList/PickerList";
import "./BehaviorPicker.scss"; import "./BehaviorPicker.scss";
interface IBehaviorPickerProps { interface IBehaviorPickerProps {
behavior: Behavior[]; behavior: Behavior[];
focusBehavior?: Behavior;
click?: (behavior: Behavior) => void;
delete?: (behavior: Behavior) => void; delete?: (behavior: Behavior) => void;
action?: (behavior: Behavior) => void; action?: (behavior: Behavior) => void;
add?: () => void; add?: (behavior: Behavior) => void;
} }
interface IBehaviorPickerState {
isPickerListOpen: boolean;
}
@useStatusWithEvent("behaviorChange")
@useSettingWithEvent("language") @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() { 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++) { for (let i = 0; i < this.props.behavior.length; i++) {
data.push({ data.push({
key: this.props.behavior[i].id, 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({ data.push({
key: "@@AddButton_List_Key", key: "@@AddButton_List_Key",
behavior: undefined behavior: undefined,
select: false
}) })
return data; return data;
} }
private renderLine = (behavior?: Behavior): ReactNode => { private renderLine = (behavior?: Behavior): ReactNode => {
if (behavior) { 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 <> return <>
<div className="behavior-picker-line-color-view"> <div className="behavior-picker-line-color-view">
<div style={{ borderLeft: `10px solid ${behavior.color}` }}/> <div style={{ borderLeft: `10px solid ${behavior.color}` }}/>
</div> </div>
<div className="behavior-picker-line-icon-view"> <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={behavior.iconName} className="behavior-icon"/>
<Icon iconName="EditCreate" className="view-icon"/> <Icon iconName="EditCreate" className="view-icon"/>
</>
}
</div> </div>
<div className={`behavior-picker-title ${this.props.setting?.language}`}> <div className={titleClassList.join(" ")}>
<div>{behavior.name}</div> <div>{behavior.name}</div>
</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"/> <Icon iconName="Delete"/>
</div> </div>
</>; </>;
} else { } else {
const openPicker = () => {
this.isInnerClick = true;
this.setState({
isPickerListOpen: true
});
}
return <> return <>
<div className="behavior-picker-line-icon-view"> <div
className="behavior-picker-line-icon-view"
onClick={openPicker}
>
<Icon iconName="Add" className="add-icon"/> <Icon iconName="Add" className="add-icon"/>
</div> </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"/> <Localization i18nKey="Behavior.Picker.Add.Button"/>
</div> </div>
</>; </>;
} }
} }
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 { public render(): ReactNode {
return <DetailsList return <>
<DetailsList
hideCheckBox hideCheckBox
className="behavior-picker-list" className="behavior-picker-list"
items={this.getData()} items={this.getData()}
clickLine={(item) => {
if (!this.isInnerClick && this.props.click && item.behavior) {
this.props.click(item.behavior);
}
this.isInnerClick = false;
}}
columns={[{ columns={[{
className: "behavior-picker-line", className: "behavior-picker-line",
key: "behavior", key: "behavior",
render: this.renderLine render: this.renderLine
}]} }]}
/> />
{this.state.isPickerListOpen ? this.renderPickerList() : null}
</>
} }
} }

View File

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

View File

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

View File

@ -38,6 +38,7 @@ interface IStatusEvent {
objectChange: void; objectChange: void;
rangeLabelChange: void; rangeLabelChange: void;
groupLabelChange: void; groupLabelChange: void;
groupBehaviorChange: void;
labelChange: void; labelChange: void;
rangeAttrChange: void; rangeAttrChange: void;
labelAttrChange: 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) { public addGroupLabel(id: ObjectID, val: Label) {
const group = this.model.getObjectById(id); const group = this.model.getObjectById(id);
if (group && group instanceof Group) { 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.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", "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.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.Title.Notfound": "{id}",
"Panel.Info.Notfound": "This panel with id {id} can not found!", "Panel.Info.Notfound": "This panel with id {id} can not found!",
"Panel.Title.Render.View": "Live preview", "Panel.Title.Render.View": "Live preview",

View File

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

View File

@ -315,8 +315,12 @@ class Group extends CtrlObject {
public addBehavior(behavior: Behavior | Behavior[]): this { public addBehavior(behavior: Behavior | Behavior[]): this {
if (Array.isArray(behavior)) { if (Array.isArray(behavior)) {
this.behaviors = this.behaviors.concat(behavior); this.behaviors = this.behaviors.concat(behavior);
for (let i = 0; i < behavior.length; i++) {
behavior[i].mount(this, this.model);
}
} else { } else {
this.behaviors.push(behavior); this.behaviors.push(behavior);
behavior.mount(this, this.model);
} }
// 按照优先级 // 按照优先级
@ -326,6 +330,27 @@ class Group extends CtrlObject {
return this; 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 * @param
@ -333,8 +358,12 @@ class Group extends CtrlObject {
public runner(t: number, effectType: "finalEffect" | "effect" | "afterEffect" ): void { public runner(t: number, effectType: "finalEffect" | "effect" | "afterEffect" ): void {
this.individuals.forEach((individual) => { this.individuals.forEach((individual) => {
for(let j = 0; j < this.behaviors.length; j++) { for(let j = 0; j < this.behaviors.length; j++) {
if (this.behaviors[j].isDeleted()) {
continue;
} else {
this.behaviors[j][effectType](individual, this, this.model, t); 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} {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> { class GroupDetails extends Component<IGroupDetailsProps & IMixinStatusProps> {
private renderFrom(group: Group) { private renderFrom(group: Group) {
@ -113,6 +116,19 @@ class GroupDetails extends Component<IGroupDetailsProps & IMixinStatusProps> {
<BehaviorPicker <BehaviorPicker
behavior={group.behaviors} 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/> <Message i18nKey="Common.Attr.Title.Individual.Generation" isTitle/>