379 lines
No EOL
13 KiB
TypeScript
379 lines
No EOL
13 KiB
TypeScript
import * as ppt from "tc-shared/PPTListener";
|
|
import {KeyDescriptor} from "tc-shared/PPTListener";
|
|
import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase";
|
|
import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events";
|
|
import * as React from "react";
|
|
import {useRef} from "react";
|
|
import {Button} from "tc-shared/ui/react-elements/Button";
|
|
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
|
import {KeyTypes, TypeCategories} from "tc-shared/KeyControl";
|
|
import {IconRenderer} from "tc-shared/ui/react-elements/Icon";
|
|
import {spawnKeySelect} from "tc-shared/ui/modal/ModalKeySelect";
|
|
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
|
import {tra} from "tc-shared/i18n/localize";
|
|
import * as keycontrol from "./../../../KeyControl";
|
|
import {MenuEntryType, spawn_context_menu} from "tc-shared/ui/elements/ContextMenu";
|
|
|
|
const cssStyle = require("./Keymap.scss");
|
|
|
|
export interface KeyMapEvents {
|
|
query_keymap: {
|
|
action: string,
|
|
query_type: "query-selected" | "general"
|
|
},
|
|
query_keymap_result: {
|
|
action: string,
|
|
status: "success" | "error" | "timeout",
|
|
|
|
error?: string,
|
|
key?: KeyDescriptor
|
|
},
|
|
|
|
set_keymap: {
|
|
action: string,
|
|
key?: KeyDescriptor
|
|
},
|
|
set_keymap_result: {
|
|
action: string,
|
|
status: "success" | "error" | "timeout",
|
|
error?: string,
|
|
key?: KeyDescriptor
|
|
},
|
|
|
|
set_selected_action: {
|
|
action: string
|
|
}
|
|
}
|
|
|
|
interface KeyActionEntryState {
|
|
assignedKey: KeyDescriptor | undefined;
|
|
selected: boolean;
|
|
state: "loading" | "applying" | "loaded" | "error";
|
|
error?: string;
|
|
}
|
|
|
|
interface KeyActionEntryProperties {
|
|
action: string;
|
|
icon: string;
|
|
description: string;
|
|
eventRegistry: Registry<KeyMapEvents>;
|
|
hidden: boolean;
|
|
}
|
|
|
|
@ReactEventHandler(e => e.props.eventRegistry)
|
|
class KeyActionEntry extends ReactComponentBase<KeyActionEntryProperties, KeyActionEntryState> {
|
|
protected defaultState(): KeyActionEntryState {
|
|
return {
|
|
assignedKey: undefined,
|
|
selected: false,
|
|
state: "loading"
|
|
};
|
|
}
|
|
|
|
componentDidMount(): void {
|
|
this.props.eventRegistry.fire("query_keymap", {action: this.props.action, query_type: "general"});
|
|
}
|
|
|
|
render() {
|
|
let rightItem;
|
|
if (this.state.state === "loading") {
|
|
rightItem =
|
|
<div key={"status-loading"} className={cssStyle.status}><Translatable>loading...</Translatable></div>;
|
|
} else if (this.state.state === "applying") {
|
|
rightItem =
|
|
<div key={"status-applying"} className={cssStyle.status}><Translatable>applying...</Translatable></div>;
|
|
} else if (this.state.state === "loaded") {
|
|
rightItem = null;
|
|
if (this.state.assignedKey)
|
|
rightItem = <div className={cssStyle.key}>{ppt.key_description(this.state.assignedKey)}</div>;
|
|
} else {
|
|
rightItem =
|
|
<div key={"status-error"} className={this.classList(cssStyle.status, cssStyle.error)}><Translatable
|
|
trIgnore={true}>{this.state.error || "unknown error"}</Translatable></div>;
|
|
}
|
|
return (
|
|
<div
|
|
key={"action-" + this.props.action}
|
|
className={this.classList(cssStyle.row, cssStyle.entry, this.state.selected ? cssStyle.selected : "")}
|
|
onClick={() => this.onClick()}
|
|
onDoubleClick={() => this.onDoubleClick()}
|
|
hidden={this.props.hidden}
|
|
onContextMenu={e => this.onContextMenu(e)}
|
|
>
|
|
<IconRenderer icon={this.props.icon}/>
|
|
<a><Translatable trIgnore={true}>{this.props.description}</Translatable></a>
|
|
{rightItem}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
private onClick() {
|
|
this.props.eventRegistry.fire("set_selected_action", {action: this.props.action});
|
|
}
|
|
|
|
private onDoubleClick() {
|
|
spawnKeySelect(key => {
|
|
if (!key) return;
|
|
|
|
this.props.eventRegistry.fire("set_keymap", {action: this.props.action, key: key});
|
|
});
|
|
}
|
|
|
|
private onContextMenu(event) {
|
|
event.preventDefault();
|
|
|
|
spawn_context_menu(event.pageX, event.pageY, {
|
|
type: MenuEntryType.ENTRY,
|
|
name: tr("Set key"),
|
|
icon_class: "client-hotkeys",
|
|
callback: () => this.onDoubleClick()
|
|
}, {
|
|
type: MenuEntryType.ENTRY,
|
|
name: tr("Remove key"),
|
|
icon_class: "client-delete",
|
|
callback: () => this.props.eventRegistry.fire("set_keymap", {action: this.props.action, key: undefined}),
|
|
visible: !!this.state.assignedKey
|
|
});
|
|
}
|
|
|
|
@EventHandler<KeyMapEvents>("set_selected_action")
|
|
private handleSelectedChange(event: KeyMapEvents["set_selected_action"]) {
|
|
this.setState({
|
|
selected: this.props.action === event.action
|
|
});
|
|
}
|
|
|
|
@EventHandler<KeyMapEvents>("query_keymap")
|
|
private handleQueryKeymap(event: KeyMapEvents["query_keymap"]) {
|
|
if (event.action !== this.props.action) return;
|
|
if (event.query_type !== "general") return;
|
|
|
|
this.setState({
|
|
state: "loading"
|
|
});
|
|
}
|
|
|
|
@EventHandler<KeyMapEvents>("query_keymap_result")
|
|
private handleQueryKeymapResult(event: KeyMapEvents["query_keymap_result"]) {
|
|
if (event.action !== this.props.action) return;
|
|
|
|
if (event.status === "success") {
|
|
this.setState({
|
|
state: "loaded",
|
|
assignedKey: event.key
|
|
});
|
|
} else {
|
|
this.setState({
|
|
state: "error",
|
|
error: event.status === "timeout" ? tr("query timeout") : event.error
|
|
});
|
|
}
|
|
}
|
|
|
|
@EventHandler<KeyMapEvents>("set_keymap")
|
|
private handleSetKeymap(event: KeyMapEvents["set_keymap"]) {
|
|
if (event.action !== this.props.action) return;
|
|
|
|
this.setState({state: "applying"});
|
|
}
|
|
|
|
@EventHandler<KeyMapEvents>("set_keymap_result")
|
|
private handleSetKeymapResult(event: KeyMapEvents["set_keymap_result"]) {
|
|
if (event.action !== this.props.action) return;
|
|
|
|
if (event.status === "success") {
|
|
this.setState({
|
|
state: "loaded",
|
|
assignedKey: event.key
|
|
});
|
|
} else {
|
|
this.setState({state: "loaded"});
|
|
createErrorModal(tr("Failed to change key"), tra("Failed to change key for action \"{}\":{:br:}{}", this.props.action, event.status === "timeout" ? tr("timeout") : event.error));
|
|
}
|
|
}
|
|
}
|
|
|
|
interface KeyActionGroupProperties {
|
|
id: string;
|
|
name: string;
|
|
eventRegistry: Registry<KeyMapEvents>;
|
|
}
|
|
|
|
class KeyActionGroup extends ReactComponentBase<KeyActionGroupProperties, { collapsed: boolean }> {
|
|
protected defaultState(): { collapsed: boolean } {
|
|
return {collapsed: false}
|
|
}
|
|
|
|
render() {
|
|
const result = [];
|
|
result.push(<div key={"category-" + this.props.id} className={this.classList(cssStyle.row, cssStyle.category)}
|
|
onClick={() => this.toggleCollapsed()}>
|
|
<div className={this.classList("arrow", this.state.collapsed ? "right" : "down")}/>
|
|
<a><Translatable trIgnore={true}>{this.props.name}</Translatable></a>
|
|
</div>);
|
|
|
|
result.push(...Object.keys(KeyTypes).filter(e => KeyTypes[e].category === this.props.id).map(e => (
|
|
<KeyActionEntry hidden={this.state.collapsed} key={e} action={e} icon={KeyTypes[e].icon}
|
|
description={KeyTypes[e].description} eventRegistry={this.props.eventRegistry}/>
|
|
)));
|
|
return result;
|
|
}
|
|
|
|
private toggleCollapsed() {
|
|
this.setState({
|
|
collapsed: !this.state.collapsed
|
|
});
|
|
}
|
|
}
|
|
|
|
interface KeyActionListProperties {
|
|
eventRegistry: Registry<KeyMapEvents>;
|
|
}
|
|
|
|
class KeyActionList extends ReactComponentBase<KeyActionListProperties, {}> {
|
|
protected defaultState(): {} {
|
|
return {};
|
|
}
|
|
|
|
render() {
|
|
const categories = [];
|
|
|
|
for (const category of Object.keys(TypeCategories)) {
|
|
categories.push(<KeyActionGroup eventRegistry={this.props.eventRegistry} key={category} id={category}
|
|
name={TypeCategories[category].name}/>)
|
|
}
|
|
|
|
return (
|
|
<div className={cssStyle.elements}>
|
|
{categories}
|
|
</div>
|
|
)
|
|
}
|
|
}
|
|
|
|
interface ButtonBarState {
|
|
active_action: string | undefined;
|
|
loading: boolean;
|
|
has_key: boolean;
|
|
}
|
|
|
|
@ReactEventHandler(e => e.props.event_registry)
|
|
class ButtonBar extends ReactComponentBase<{ event_registry: Registry<KeyMapEvents> }, ButtonBarState> {
|
|
protected defaultState(): ButtonBarState {
|
|
return {
|
|
active_action: undefined,
|
|
loading: true,
|
|
has_key: false
|
|
};
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<div className={cssStyle.buttons}>
|
|
<Button color={"red"} disabled={!this.state.active_action || this.state.loading || !this.state.has_key}
|
|
onClick={() => this.onButtonClick()}>
|
|
<Translatable>Clear Key</Translatable>
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
@EventHandler<KeyMapEvents>("set_selected_action")
|
|
private handleSetSelectedAction(event: KeyMapEvents["set_selected_action"]) {
|
|
this.setState({
|
|
active_action: event.action,
|
|
loading: true
|
|
}, () => {
|
|
this.props.event_registry.fire("query_keymap", {action: event.action, query_type: "query-selected"});
|
|
});
|
|
}
|
|
|
|
@EventHandler<KeyMapEvents>("query_keymap_result")
|
|
private handleQueryKeymapResult(event: KeyMapEvents["query_keymap_result"]) {
|
|
this.setState({
|
|
loading: false,
|
|
has_key: event.status === "success" && !!event.key
|
|
});
|
|
}
|
|
|
|
private onButtonClick() {
|
|
this.props.event_registry.fire("set_keymap", {action: this.state.active_action, key: undefined});
|
|
}
|
|
}
|
|
|
|
export const KeyMapSettings = () => {
|
|
const events = useRef<Registry<KeyMapEvents>>(undefined);
|
|
|
|
if (events.current === undefined) {
|
|
events.current = new Registry<KeyMapEvents>();
|
|
initialize_timeouts(events.current);
|
|
initialize_controller(events.current);
|
|
}
|
|
|
|
return (<>
|
|
<div key={"header"} className={cssStyle.header}>
|
|
<a><Translatable>Keymap</Translatable></a>
|
|
</div>
|
|
<div key={"body"} className={cssStyle.containerList}>
|
|
<KeyActionList eventRegistry={events.current}/>
|
|
<ButtonBar event_registry={events.current}/>
|
|
</div>
|
|
</>);
|
|
};
|
|
|
|
function initialize_timeouts(event_registry: Registry<KeyMapEvents>) {
|
|
/* query */
|
|
{
|
|
let timeouts = {};
|
|
event_registry.on("query_keymap", event => {
|
|
clearTimeout(timeouts[event.action]);
|
|
timeouts[event.action] = setTimeout(() => {
|
|
event_registry.fire("query_keymap_result", {action: event.action, status: "timeout"});
|
|
}, 5000);
|
|
});
|
|
event_registry.on("query_keymap_result", event => {
|
|
clearTimeout(timeouts[event.action]);
|
|
delete timeouts[event.action];
|
|
});
|
|
}
|
|
|
|
/* change */
|
|
{
|
|
let timeouts = {};
|
|
event_registry.on("set_keymap", event => {
|
|
clearTimeout(timeouts[event.action]);
|
|
timeouts[event.action] = setTimeout(() => {
|
|
event_registry.fire("set_keymap_result", {action: event.action, status: "timeout"});
|
|
}, 5000);
|
|
});
|
|
|
|
event_registry.on("set_keymap_result", event => {
|
|
clearTimeout(timeouts[event.action]);
|
|
delete timeouts[event.action];
|
|
});
|
|
}
|
|
}
|
|
|
|
function initialize_controller(event_registry: Registry<KeyMapEvents>) {
|
|
event_registry.on("query_keymap", event => {
|
|
event_registry.fire_async("query_keymap_result", {
|
|
status: "success",
|
|
action: event.action,
|
|
key: keycontrol.key(event.action)
|
|
});
|
|
});
|
|
|
|
event_registry.on("set_keymap", event => {
|
|
try {
|
|
keycontrol.set_key(event.action, event.key);
|
|
event_registry.fire_async("set_keymap_result", {status: "success", action: event.action, key: event.key});
|
|
} catch (error) {
|
|
console.warn("Failed to change key for action %s: %o", event.action, error);
|
|
event_registry.fire_async("set_keymap_result", {
|
|
status: "error",
|
|
action: event.action,
|
|
error: error instanceof Error ? error.message : error?.toString()
|
|
});
|
|
}
|
|
})
|
|
} |