371 lines
No EOL
12 KiB
TypeScript
371 lines
No EOL
12 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 {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 message={"loading..."} /></div>;
|
|
} else if(this.state.state === "applying") {
|
|
rightItem = <div key={"status-applying"} className={cssStyle.status}><Translatable message={"applying..."} /></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 message={this.state.error || "unknown error"} /></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 message={this.props.description} /></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 message={this.props.name} /></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 message={"Clear Key"} />
|
|
</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 class KeyMapSettings extends React.Component<{}, {}> {
|
|
private readonly event_registry: Registry<KeyMapEvents>;
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.event_registry = new Registry<KeyMapEvents>();
|
|
initialize_timeouts(this.event_registry);
|
|
initialize_controller(this.event_registry);
|
|
}
|
|
|
|
render() {
|
|
//TODO: May refresh button?
|
|
return [
|
|
<div key={"header"} className={cssStyle.header}>
|
|
<a><Translatable message={"Keymap"} /></a>
|
|
</div>,
|
|
<div key={"body"} className={cssStyle.containerList}>
|
|
<KeyActionList eventRegistry={this.event_registry} />
|
|
<ButtonBar event_registry={this.event_registry} />
|
|
</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() });
|
|
}
|
|
})
|
|
} |