Added hotkeys to the client
This commit is contained in:
parent
6fc7f9a8dc
commit
3b500daac7
20 changed files with 950 additions and 51 deletions
|
@ -1,4 +1,8 @@
|
||||||
# Changelog:
|
# Changelog:
|
||||||
|
* **10.03.20**
|
||||||
|
- Improved key code displaying
|
||||||
|
- Added a keymap system (Hotkeys)
|
||||||
|
|
||||||
* **09.03.20**
|
* **09.03.20**
|
||||||
- Using React for the client control bar
|
- Using React for the client control bar
|
||||||
- Saving last away state and message
|
- Saving last away state and message
|
||||||
|
|
|
@ -1959,6 +1959,7 @@
|
||||||
<div class="entry" container="general-language">{{tr "Language" /}}</div>
|
<div class="entry" container="general-language">{{tr "Language" /}}</div>
|
||||||
<!-- <div class="entry" container="general-updates">{{tr "Updates" /}}</div> -->
|
<!-- <div class="entry" container="general-updates">{{tr "Updates" /}}</div> -->
|
||||||
<div class="entry" container="general-chat">{{tr "Chat" /}}</div>
|
<div class="entry" container="general-chat">{{tr "Chat" /}}</div>
|
||||||
|
<div class="entry" container="general-keymap">{{tr "Keymap" /}}</div>
|
||||||
|
|
||||||
<div class="entry group">{{tr "Audio" /}}</div>
|
<div class="entry group">{{tr "Audio" /}}</div>
|
||||||
<div class="entry" container="audio-microphone">{{tr "Microphone" /}}</div>
|
<div class="entry" container="audio-microphone">{{tr "Microphone" /}}</div>
|
||||||
|
@ -2019,6 +2020,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container general-updates">{{tr "GU" /}}</div>
|
<div class="container general-updates">{{tr "GU" /}}</div>
|
||||||
|
<div class="container general-keymap"></div>
|
||||||
<div class="container general-chat">
|
<div class="container general-chat">
|
||||||
<label>
|
<label>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
|
|
|
@ -12,7 +12,8 @@ export interface BroadcastMessage {
|
||||||
|
|
||||||
function uuidv4() {
|
function uuidv4() {
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||||
const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
const r = Math.random() * 16 | 0;
|
||||||
|
const v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||||||
return v.toString(16);
|
return v.toString(16);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -317,6 +317,8 @@ export class ConnectionHandler {
|
||||||
|
|
||||||
async disconnectFromServer(reason?: string) {
|
async disconnectFromServer(reason?: string) {
|
||||||
this.cancel_reconnect(true);
|
this.cancel_reconnect(true);
|
||||||
|
if(!this.connected) return;
|
||||||
|
|
||||||
this.handleDisconnect(DisconnectReason.REQUESTED); //TODO message?
|
this.handleDisconnect(DisconnectReason.REQUESTED); //TODO message?
|
||||||
try {
|
try {
|
||||||
await this.serverConnection.disconnect();
|
await this.serverConnection.disconnect();
|
||||||
|
@ -977,7 +979,7 @@ export class ConnectionHandler {
|
||||||
this.sound.play(muted ? Sound.MICROPHONE_MUTED : Sound.MICROPHONE_ACTIVATED);
|
this.sound.play(muted ? Sound.MICROPHONE_MUTED : Sound.MICROPHONE_ACTIVATED);
|
||||||
this.update_voice_status();
|
this.update_voice_status();
|
||||||
}
|
}
|
||||||
|
toggleMicrophone() { this.setMicrophoneMuted(!this.isMicrophoneMuted()); }
|
||||||
isMicrophoneMuted() { return this.client_status.input_muted; }
|
isMicrophoneMuted() { return this.client_status.input_muted; }
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -996,6 +998,7 @@ export class ConnectionHandler {
|
||||||
this.update_voice_status();
|
this.update_voice_status();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleSpeakerMuted() { this.setSpeakerMuted(!this.isSpeakerMuted()); }
|
||||||
isSpeakerMuted() { return this.client_status.output_muted; }
|
isSpeakerMuted() { return this.client_status.output_muted; }
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -1044,7 +1047,7 @@ export class ConnectionHandler {
|
||||||
state: "away"
|
state: "away"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
toggleAway() { this.setAway(!this.isAway()); }
|
||||||
isAway() : boolean { return typeof this.client_status.away !== "boolean" || this.client_status.away; }
|
isAway() : boolean { return typeof this.client_status.away !== "boolean" || this.client_status.away; }
|
||||||
|
|
||||||
setQueriesShown(flag: boolean) {
|
setQueriesShown(flag: boolean) {
|
||||||
|
|
199
shared/js/KeyControl.ts
Normal file
199
shared/js/KeyControl.ts
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
|
||||||
|
import {EventType, KeyDescriptor, KeyEvent, KeyHook} from "tc-shared/PPTListener";
|
||||||
|
import * as ppt from "tc-backend/ppt";
|
||||||
|
import {Settings, settings} from "tc-shared/settings";
|
||||||
|
import * as log from "tc-shared/log";
|
||||||
|
import {LogCategory} from "tc-shared/log";
|
||||||
|
|
||||||
|
export interface KeyControl {
|
||||||
|
category: string;
|
||||||
|
description: string;
|
||||||
|
handler: () => void;
|
||||||
|
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TypeCategories: {[key: string]: { name: string }} = {
|
||||||
|
"connection": {
|
||||||
|
name: "Server connection"
|
||||||
|
},
|
||||||
|
"microphone": {
|
||||||
|
name: "Microphone"
|
||||||
|
},
|
||||||
|
"speaker": {
|
||||||
|
name: "Speaker"
|
||||||
|
},
|
||||||
|
"away": {
|
||||||
|
name: "Away"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const KeyTypes: {[key: string]:KeyControl} = {
|
||||||
|
"disconnect-current": {
|
||||||
|
category: "connection",
|
||||||
|
description: "Disconnect from the current server",
|
||||||
|
handler: () => server_connections.active_connection()?.disconnectFromServer(),
|
||||||
|
icon: "client-disconnect"
|
||||||
|
},
|
||||||
|
"disconnect-all": {
|
||||||
|
category: "connection",
|
||||||
|
description: "Disconnect from all connected servers",
|
||||||
|
handler: () => server_connections.all_connections().forEach(e => e.disconnectFromServer()),
|
||||||
|
icon: "client-disconnect"
|
||||||
|
},
|
||||||
|
|
||||||
|
"toggle-microphone": {
|
||||||
|
category: "microphone",
|
||||||
|
description: "Toggle your microphone status",
|
||||||
|
handler: () => server_connections.active_connection()?.toggleMicrophone(),
|
||||||
|
icon: "client-input_muted"
|
||||||
|
},
|
||||||
|
"enable-microphone": {
|
||||||
|
category: "microphone",
|
||||||
|
description: "Enable your microphone",
|
||||||
|
handler: () => server_connections.active_connection()?.setMicrophoneMuted(false),
|
||||||
|
icon: "client-input_muted"
|
||||||
|
},
|
||||||
|
"disable-microphone": {
|
||||||
|
category: "microphone",
|
||||||
|
description: "Disable your microphone",
|
||||||
|
handler: () => server_connections.active_connection()?.setMicrophoneMuted(true),
|
||||||
|
icon: "client-input_muted"
|
||||||
|
},
|
||||||
|
|
||||||
|
"toggle-speaker": {
|
||||||
|
category: "speaker",
|
||||||
|
description: "Toggle your speaker status",
|
||||||
|
handler: () => server_connections.active_connection()?.toggleSpeakerMuted(),
|
||||||
|
icon: "client-output_muted"
|
||||||
|
},
|
||||||
|
"enable-speaker": {
|
||||||
|
category: "speaker",
|
||||||
|
description: "Enable your speakers",
|
||||||
|
handler: () => server_connections.active_connection()?.setSpeakerMuted(false),
|
||||||
|
icon: "client-output_muted"
|
||||||
|
},
|
||||||
|
"disable-speaker": {
|
||||||
|
category: "speaker",
|
||||||
|
description: "Disable your speakers",
|
||||||
|
handler: () => server_connections.active_connection()?.setSpeakerMuted(true),
|
||||||
|
icon: "client-output_muted"
|
||||||
|
},
|
||||||
|
|
||||||
|
/* toggle away */
|
||||||
|
"toggle-away-state": {
|
||||||
|
category: "away",
|
||||||
|
description: "Toggle your away state",
|
||||||
|
handler: () => server_connections.active_connection()?.toggleAway(),
|
||||||
|
icon: "client-away"
|
||||||
|
},
|
||||||
|
"enable-away-state": {
|
||||||
|
category: "away",
|
||||||
|
description: "Enable away for the current server",
|
||||||
|
handler: () => server_connections.active_connection()?.setAway(true),
|
||||||
|
icon: "client-away"
|
||||||
|
},
|
||||||
|
"disable-away-state": {
|
||||||
|
category: "away",
|
||||||
|
description: "Disable away for the current server",
|
||||||
|
handler: () => server_connections.active_connection()?.setAway(false),
|
||||||
|
icon: "client-present"
|
||||||
|
},
|
||||||
|
"toggle-away-state-globally": {
|
||||||
|
category: "away",
|
||||||
|
description: "Toggle your away state for every server",
|
||||||
|
handler: () => server_connections.all_connections().forEach(e => e.toggleAway()),
|
||||||
|
icon: "client-away"
|
||||||
|
},
|
||||||
|
"enable-away-state-globally": {
|
||||||
|
category: "away",
|
||||||
|
description: "Enable away for every server",
|
||||||
|
handler: () => server_connections.all_connections().forEach(e => e.setAway(true)),
|
||||||
|
icon: "client-away"
|
||||||
|
},
|
||||||
|
"disable-away-state-globally": {
|
||||||
|
category: "away",
|
||||||
|
description: "Disable away for every server",
|
||||||
|
handler: () => server_connections.all_connections().forEach(e => e.setAway(false)),
|
||||||
|
icon: "client-present"
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let key_bindings: {[key: string]: {
|
||||||
|
binding: KeyDescriptor,
|
||||||
|
hook: KeyHook
|
||||||
|
}} = {};
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
version?: number;
|
||||||
|
|
||||||
|
keys?: {[key: string]: KeyDescriptor};
|
||||||
|
}
|
||||||
|
|
||||||
|
let config: Config;
|
||||||
|
export function initialize() {
|
||||||
|
let cfg: Config;
|
||||||
|
try {
|
||||||
|
cfg = JSON.parse(settings.global(Settings.KEY_KEYCONTROL_DATA));
|
||||||
|
} catch (e) {
|
||||||
|
log.error(LogCategory.GENERAL, tr("Failed to parse old key control data."));
|
||||||
|
cfg = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if(typeof cfg.version !== "number") {
|
||||||
|
/* new config */
|
||||||
|
cfg.version = 0;
|
||||||
|
}
|
||||||
|
switch (cfg.version) {
|
||||||
|
case 0:
|
||||||
|
cfg.version = 1;
|
||||||
|
cfg.keys = {};
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
config = cfg;
|
||||||
|
|
||||||
|
for(const key of Object.keys(config.keys)) {
|
||||||
|
if(typeof KeyTypes[key] !== "object")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
bind_key(key, config.keys[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function save_config() {
|
||||||
|
settings.changeGlobal(Settings.KEY_KEYCONTROL_DATA, JSON.stringify(config));
|
||||||
|
}
|
||||||
|
|
||||||
|
function bind_key(action: string, key: KeyDescriptor) {
|
||||||
|
const control = KeyTypes[action];
|
||||||
|
if(typeof control === "undefined")
|
||||||
|
throw "missing control event";
|
||||||
|
|
||||||
|
key_bindings[action] = {
|
||||||
|
hook: Object.assign({
|
||||||
|
callback_press: () => control.handler(),
|
||||||
|
callback_release: () => {},
|
||||||
|
cancel: false
|
||||||
|
}, key),
|
||||||
|
binding: key
|
||||||
|
};
|
||||||
|
ppt.register_key_hook(key_bindings[action].hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function set_key(action: string, key?: KeyDescriptor) {
|
||||||
|
if(typeof key_bindings[action] !== "undefined") {
|
||||||
|
ppt.unregister_key_hook(key_bindings[action].hook);
|
||||||
|
delete key_bindings[action];
|
||||||
|
}
|
||||||
|
if(key) {
|
||||||
|
bind_key(action, key);
|
||||||
|
config.keys[action] = key;
|
||||||
|
} else {
|
||||||
|
delete config.keys[action];
|
||||||
|
}
|
||||||
|
|
||||||
|
save_config();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function key(action: string) : KeyDescriptor | undefined { return key_bindings[action]?.binding; }
|
|
@ -168,7 +168,17 @@ export function key_description(key: KeyDescriptor) {
|
||||||
if(!result && !key.key_code)
|
if(!result && !key.key_code)
|
||||||
return tr("unset");
|
return tr("unset");
|
||||||
|
|
||||||
if(key.key_code)
|
if(key.key_code) {
|
||||||
result += " + " + key.key_code;
|
let key_name;
|
||||||
|
if(key.key_code.startsWith("Key"))
|
||||||
|
key_name = key.key_code.substr(3);
|
||||||
|
else if(key.key_code.startsWith("Digit"))
|
||||||
|
key_name = key.key_code.substr(5);
|
||||||
|
else if(key.key_code.startsWith("Numpad"))
|
||||||
|
key_name = "Numpad " + key.key_code.substr(6);
|
||||||
|
else
|
||||||
|
key_name = key.key_code;
|
||||||
|
result += " + " + key_name;
|
||||||
|
}
|
||||||
return result.substr(3);
|
return result.substr(3);
|
||||||
}
|
}
|
|
@ -24,13 +24,13 @@ import {openModalNewcomer} from "tc-shared/ui/modal/ModalNewcomer";
|
||||||
import * as aplayer from "tc-backend/audio/player";
|
import * as aplayer from "tc-backend/audio/player";
|
||||||
import * as arecorder from "tc-backend/audio/recorder";
|
import * as arecorder from "tc-backend/audio/recorder";
|
||||||
import * as ppt from "tc-backend/ppt";
|
import * as ppt from "tc-backend/ppt";
|
||||||
|
import * as keycontrol from "./KeyControl";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
import * as ReactDOM from "react-dom";
|
||||||
import * as cbar from "./ui/frames/control-bar";
|
import * as cbar from "./ui/frames/control-bar";
|
||||||
import * as global_ev_handler from "./events/ClientGlobalControlHandler";
|
import * as global_ev_handler from "./events/ClientGlobalControlHandler";
|
||||||
import {Registry} from "tc-shared/events";
|
|
||||||
import {ClientGlobalControlEvents, global_client_actions} from "tc-shared/events/GlobalEvents";
|
import {ClientGlobalControlEvents, global_client_actions} from "tc-shared/events/GlobalEvents";
|
||||||
|
import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings";
|
||||||
|
|
||||||
/* required import for init */
|
/* required import for init */
|
||||||
require("./proto").initialize();
|
require("./proto").initialize();
|
||||||
|
@ -338,6 +338,7 @@ function main() {
|
||||||
$(".window-resize-listener").trigger('resize');
|
$(".window-resize-listener").trigger('resize');
|
||||||
}, 1000);
|
}, 1000);
|
||||||
});
|
});
|
||||||
|
keycontrol.initialize();
|
||||||
|
|
||||||
stats.initialize({
|
stats.initialize({
|
||||||
verbose: true,
|
verbose: true,
|
||||||
|
@ -400,7 +401,7 @@ function main() {
|
||||||
], () => {});
|
], () => {});
|
||||||
*/
|
*/
|
||||||
}, 4000);
|
}, 4000);
|
||||||
//Modals.spawnSettingsModal("identity-profiles");
|
spawnSettingsModal("general-keymap");
|
||||||
//Modals.spawnKeySelect(console.log);
|
//Modals.spawnKeySelect(console.log);
|
||||||
//Modals.spawnBookmarkModal();
|
//Modals.spawnBookmarkModal();
|
||||||
|
|
||||||
|
|
|
@ -343,6 +343,11 @@ export class Settings extends StaticSettings {
|
||||||
default_value: 100
|
default_value: 100
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static readonly KEY_KEYCONTROL_DATA: SettingsKey<string> = {
|
||||||
|
key: "keycontrol_data",
|
||||||
|
default_value: "{}"
|
||||||
|
};
|
||||||
|
|
||||||
static readonly KEY_LAST_INVITE_LINK_TYPE: SettingsKey<string> = {
|
static readonly KEY_LAST_INVITE_LINK_TYPE: SettingsKey<string> = {
|
||||||
key: "last_invite_link_type",
|
key: "last_invite_link_type",
|
||||||
default_value: "tea-web"
|
default_value: "tea-web"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {ReactComponentBase} from "tc-shared/ui/elements/ReactComponentBase";
|
import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase";
|
||||||
import {DropdownContainer} from "tc-shared/ui/frames/control-bar/dropdown";
|
import {DropdownContainer} from "tc-shared/ui/frames/control-bar/dropdown";
|
||||||
const cssStyle = require("./button.scss");
|
const cssStyle = require("./button.scss");
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {ReactComponentBase} from "tc-shared/ui/elements/ReactComponentBase";
|
import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase";
|
||||||
|
import {IconRenderer} from "tc-shared/ui/react-elements/Icon";
|
||||||
const cssStyle = require("./button.scss");
|
const cssStyle = require("./button.scss");
|
||||||
|
|
||||||
export interface DropdownEntryProperties {
|
export interface DropdownEntryProperties {
|
||||||
|
@ -10,36 +11,6 @@ export interface DropdownEntryProperties {
|
||||||
onContextMenu?: (event) => void;
|
onContextMenu?: (event) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class IconRenderer extends React.Component<{ icon: string | JQuery<HTMLDivElement> }, {}> {
|
|
||||||
private readonly icon_ref: React.RefObject<HTMLDivElement>;
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
if(typeof this.props.icon === "object")
|
|
||||||
this.icon_ref = React.createRef();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if(!this.props.icon)
|
|
||||||
return <div className={"icon-container icon-empty"} />;
|
|
||||||
else if(typeof this.props.icon === "string")
|
|
||||||
return <div className={"icon " + this.props.icon} />;
|
|
||||||
|
|
||||||
|
|
||||||
return <div ref={this.icon_ref} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount(): void {
|
|
||||||
if(this.icon_ref)
|
|
||||||
$(this.icon_ref.current).replaceWith(this.props.icon);
|
|
||||||
}
|
|
||||||
componentWillUnmount(): void {
|
|
||||||
if(this.icon_ref)
|
|
||||||
$(this.icon_ref.current).empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DropdownEntry extends ReactComponentBase<DropdownEntryProperties, {}> {
|
export class DropdownEntry extends ReactComponentBase<DropdownEntryProperties, {}> {
|
||||||
protected default_state() { return {}; }
|
protected default_state() { return {}; }
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {Button} from "./button";
|
import {Button} from "./button";
|
||||||
import {DropdownEntry} from "tc-shared/ui/frames/control-bar/dropdown";
|
import {DropdownEntry} from "tc-shared/ui/frames/control-bar/dropdown";
|
||||||
import {Translatable} from "tc-shared/ui/elements/i18n";
|
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
import {ReactComponentBase} from "tc-shared/ui/elements/ReactComponentBase";
|
import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase";
|
||||||
import {ConnectionEvents, ConnectionHandler, ConnectionStateUpdateType} from "tc-shared/ConnectionHandler";
|
import {ConnectionEvents, ConnectionHandler, ConnectionStateUpdateType} from "tc-shared/ConnectionHandler";
|
||||||
import {Event, EventHandler, ReactEventHandler, Registry} from "tc-shared/events";
|
import {Event, EventHandler, ReactEventHandler, Registry} from "tc-shared/events";
|
||||||
import {ConnectionManagerEvents, server_connections} from "tc-shared/ui/frames/connection_handlers";
|
import {ConnectionManagerEvents, server_connections} from "tc-shared/ui/frames/connection_handlers";
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import {createModal} from "tc-shared/ui/elements/Modal";
|
import {createModal} from "tc-shared/ui/elements/Modal";
|
||||||
import {EventType, key_description, KeyEvent} from "tc-shared/PPTListener";
|
import {EventType, key_description, KeyEvent} from "tc-shared/PPTListener";
|
||||||
import * as loader from "tc-loader";
|
|
||||||
import * as ppt from "tc-backend/ppt";
|
import * as ppt from "tc-backend/ppt";
|
||||||
|
|
||||||
export function spawnKeySelect(callback: (key?: KeyEvent) => void) {
|
export function spawnKeySelect(callback: (key?: KeyEvent) => void) {
|
||||||
|
|
|
@ -30,6 +30,9 @@ import * as loader from "tc-loader";
|
||||||
import * as aplayer from "tc-backend/audio/player";
|
import * as aplayer from "tc-backend/audio/player";
|
||||||
import * as arecorder from "tc-backend/audio/recorder";
|
import * as arecorder from "tc-backend/audio/recorder";
|
||||||
import * as ppt from "tc-backend/ppt";
|
import * as ppt from "tc-backend/ppt";
|
||||||
|
import {KeyMapSettings} from "tc-shared/ui/modal/settings/Keymap";
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ReactDOM from "react-dom";
|
||||||
|
|
||||||
export function spawnSettingsModal(default_page?: string) : Modal {
|
export function spawnSettingsModal(default_page?: string) : Modal {
|
||||||
let modal: Modal;
|
let modal: Modal;
|
||||||
|
@ -71,6 +74,7 @@ export function spawnSettingsModal(default_page?: string) : Modal {
|
||||||
settings_general_application(modal.htmlTag.find(".right .container.general-application"), modal);
|
settings_general_application(modal.htmlTag.find(".right .container.general-application"), modal);
|
||||||
settings_general_language(modal.htmlTag.find(".right .container.general-language"), modal);
|
settings_general_language(modal.htmlTag.find(".right .container.general-language"), modal);
|
||||||
settings_general_chat(modal.htmlTag.find(".right .container.general-chat"), modal);
|
settings_general_chat(modal.htmlTag.find(".right .container.general-chat"), modal);
|
||||||
|
settings_general_keymap(modal.htmlTag.find(".right .container.general-keymap"), modal);
|
||||||
settings_audio_microphone(modal.htmlTag.find(".right .container.audio-microphone"), modal);
|
settings_audio_microphone(modal.htmlTag.find(".right .container.audio-microphone"), modal);
|
||||||
settings_audio_speaker(modal.htmlTag.find(".right .container.audio-speaker"), modal);
|
settings_audio_speaker(modal.htmlTag.find(".right .container.audio-speaker"), modal);
|
||||||
settings_audio_sounds(modal.htmlTag.find(".right .container.audio-sounds"), modal);
|
settings_audio_sounds(modal.htmlTag.find(".right .container.audio-sounds"), modal);
|
||||||
|
@ -342,6 +346,12 @@ function settings_general_language(container: JQuery, modal: Modal) {
|
||||||
update_current_selected();
|
update_current_selected();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function settings_general_keymap(container: JQuery, modal: Modal) {
|
||||||
|
const entry = <KeyMapSettings />;
|
||||||
|
ReactDOM.render(entry, container[0]);
|
||||||
|
modal.close_listener.push(() => ReactDOM.hydrate(entry, container[0])); //FIXME: hydrate does not work!
|
||||||
|
}
|
||||||
|
|
||||||
function settings_general_chat(container: JQuery, modal: Modal) {
|
function settings_general_chat(container: JQuery, modal: Modal) {
|
||||||
/* timestamp format */
|
/* timestamp format */
|
||||||
{
|
{
|
||||||
|
@ -587,9 +597,12 @@ function settings_audio_sounds(contianer: JQuery, modal: Modal) {
|
||||||
/* initialize sound list */
|
/* initialize sound list */
|
||||||
{
|
{
|
||||||
const container_sounds = contianer.find(".container-sounds");
|
const container_sounds = contianer.find(".container-sounds");
|
||||||
|
let scrollbar;
|
||||||
|
/*
|
||||||
let scrollbar: SimpleBar;
|
let scrollbar: SimpleBar;
|
||||||
if("SimpleBar" in window)
|
if("SimpleBar" in window)
|
||||||
scrollbar = new SimpleBar(container_sounds[0]);
|
scrollbar = new SimpleBar(container_sounds[0]);
|
||||||
|
*/
|
||||||
|
|
||||||
const generate_sound = (_sound: Sound) => {
|
const generate_sound = (_sound: Sound) => {
|
||||||
let tag_play_pause: JQuery, tag_play: JQuery, tag_pause: JQuery, tag_input_muted: JQuery;
|
let tag_play_pause: JQuery, tag_play: JQuery, tag_pause: JQuery, tag_input_muted: JQuery;
|
||||||
|
@ -662,13 +675,13 @@ function settings_audio_sounds(contianer: JQuery, modal: Modal) {
|
||||||
|
|
||||||
const overlap_tag = contianer.find(".option-overlap-same");
|
const overlap_tag = contianer.find(".option-overlap-same");
|
||||||
overlap_tag.on('change', event => {
|
overlap_tag.on('change', event => {
|
||||||
const activated = (<HTMLInputElement>event.target).checked;
|
const activated = (event.target as HTMLInputElement).checked;
|
||||||
sound.set_overlap_activated(activated);
|
sound.set_overlap_activated(activated);
|
||||||
}).prop("checked", sound.overlap_activated());
|
}).prop("checked", sound.overlap_activated());
|
||||||
|
|
||||||
const mute_tag = contianer.find(".option-mute-output");
|
const mute_tag = contianer.find(".option-mute-output");
|
||||||
mute_tag.on('change', event => {
|
mute_tag.on('change', event => {
|
||||||
const activated = (<HTMLInputElement>event.target).checked;
|
const activated = (event.target as HTMLInputElement).checked;
|
||||||
sound.set_ignore_output_muted(!activated);
|
sound.set_ignore_output_muted(!activated);
|
||||||
}).prop("checked", !sound.ignore_output_muted());
|
}).prop("checked", !sound.ignore_output_muted());
|
||||||
|
|
||||||
|
@ -2031,10 +2044,10 @@ export namespace modal_settings {
|
||||||
let last_value;
|
let last_value;
|
||||||
|
|
||||||
container_select.find("input").on('change', event => {
|
container_select.find("input").on('change', event => {
|
||||||
if(!(<HTMLInputElement>event.target).checked)
|
if(!(event.target as HTMLInputElement).checked)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const mode = (<HTMLInputElement>event.target).value;
|
const mode = (event.target as HTMLInputElement).value;
|
||||||
if(mode === last_value) return;
|
if(mode === last_value) return;
|
||||||
|
|
||||||
event_registry.fire("set-setting", { setting: "vad-type", value: mode });
|
event_registry.fire("set-setting", { setting: "vad-type", value: mode });
|
167
shared/js/ui/modal/settings/Keymap.scss
Normal file
167
shared/js/ui/modal/settings/Keymap.scss
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
@import "../../../../css/static/mixin.scss";
|
||||||
|
@import "../../../../css/static/properties.scss";
|
||||||
|
|
||||||
|
.containerList {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
min-height: 6em;
|
||||||
|
|
||||||
|
background-color: $color_list_background;
|
||||||
|
border: 1px $color_list_border solid;
|
||||||
|
|
||||||
|
border-radius: $border_radius_large;
|
||||||
|
|
||||||
|
.elements {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
padding-top: .5em;
|
||||||
|
padding-bottom: .5em;
|
||||||
|
|
||||||
|
@include chat-scrollbar-vertical();
|
||||||
|
|
||||||
|
.row {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
height: 1.5em;
|
||||||
|
|
||||||
|
padding-right: .5em;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.category {
|
||||||
|
padding-left: .25em;
|
||||||
|
|
||||||
|
:global .arrow {
|
||||||
|
align-self: center;
|
||||||
|
margin: .3em;
|
||||||
|
/*
|
||||||
|
margin-right: .5em;
|
||||||
|
|
||||||
|
&.down {
|
||||||
|
margin-bottom: .25em;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
margin-left: .2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.entry {
|
||||||
|
padding-left: 2em;
|
||||||
|
|
||||||
|
:global .icon {
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
margin-right: .25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key {
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
color: #464646;
|
||||||
|
background-color: #17171a;
|
||||||
|
border-radius: .2em;
|
||||||
|
|
||||||
|
height: 1.5em;
|
||||||
|
font-size: .7em;
|
||||||
|
|
||||||
|
padding-left: .35em;
|
||||||
|
padding-right: .35em;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
font-size: .7em;
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
color: #a10000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
min-width: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $color_list_hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: $color_list_selected;
|
||||||
|
|
||||||
|
.key {
|
||||||
|
background-color: #0e0e10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.row[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
background-color: #242527;
|
||||||
|
|
||||||
|
padding: .5em;
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #161616;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
height: 3em;
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
|
||||||
|
a {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
align-self: flex-end;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #e0e0e0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
371
shared/js/ui/modal/settings/Keymap.tsx
Normal file
371
shared/js/ui/modal/settings/Keymap.tsx
Normal file
|
@ -0,0 +1,371 @@
|
||||||
|
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 default_state() : 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.updateState({
|
||||||
|
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.updateState({
|
||||||
|
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.updateState({
|
||||||
|
state: "loaded",
|
||||||
|
assignedKey: event.key
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.updateState({
|
||||||
|
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.updateState({ 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.updateState({
|
||||||
|
state: "loaded",
|
||||||
|
assignedKey: event.key
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.updateState({ 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 default_state(): { 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.updateState({
|
||||||
|
collapsed: !this.state.collapsed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KeyActionListProperties {
|
||||||
|
eventRegistry: Registry<KeyMapEvents>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class KeyActionList extends ReactComponentBase<KeyActionListProperties, {}> {
|
||||||
|
protected default_state(): {} {
|
||||||
|
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 default_state(): 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.updateState({
|
||||||
|
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.updateState({
|
||||||
|
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() });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
76
shared/js/ui/react-elements/Button.scss
Normal file
76
shared/js/ui/react-elements/Button.scss
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
@import "../../../css/static/mixin.scss";
|
||||||
|
@import "../../../css/static/properties.scss";
|
||||||
|
|
||||||
|
.button {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
|
||||||
|
border-width: 0;
|
||||||
|
border-radius: $border_radius_middle;
|
||||||
|
border-style: solid;
|
||||||
|
|
||||||
|
color: #7c7c7c;
|
||||||
|
|
||||||
|
height: 2.2em;
|
||||||
|
padding: .25em 1em;
|
||||||
|
|
||||||
|
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14), 0 3px 1px -2px rgba(0, 0, 0, .2), 0 1px 5px 0 rgba(0, 0, 0, .12);
|
||||||
|
|
||||||
|
@include text-dotdotdot();
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #0a0a0a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
box-shadow: none;
|
||||||
|
background-color: rgba(0, 0, 0, 0.27);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.27);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.color-green {
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
border-bottom-color: #389738;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.color-blue, &.color-default {
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
border-bottom-color: #386896;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.color-red {
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
border-bottom-color: #973838;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.color-purple {
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
border-bottom-color: #5f3586;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.color-brown {
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
border-bottom-color: #965238;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.color-yellow {
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
border-bottom-color: #96903a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.type-normal { }
|
||||||
|
|
||||||
|
&.type-small {
|
||||||
|
font-size: .9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.type-extra-small {
|
||||||
|
font-size: .6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include transition(background-color $button_hover_animation_time ease-in-out);
|
||||||
|
}
|
40
shared/js/ui/react-elements/Button.tsx
Normal file
40
shared/js/ui/react-elements/Button.tsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase";
|
||||||
|
import * as React from "react";
|
||||||
|
const cssStyle = require("./Button.scss");
|
||||||
|
|
||||||
|
export interface ButtonProperties {
|
||||||
|
color?: "green" | "blue" | "red" | "purple" | "brown" | "yellow" | "default";
|
||||||
|
type?: "normal" | "small" | "extra-small";
|
||||||
|
|
||||||
|
onClick?: () => void;
|
||||||
|
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ButtonState {
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Button extends ReactComponentBase<ButtonProperties, ButtonState> {
|
||||||
|
protected default_state(): ButtonState {
|
||||||
|
return {
|
||||||
|
disabled: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={this.classList(
|
||||||
|
cssStyle.button,
|
||||||
|
cssStyle["color-" + this.props.color] || cssStyle["color-default"],
|
||||||
|
cssStyle["type-" + this.props.type] || cssStyle["type-normal"]
|
||||||
|
)}
|
||||||
|
disabled={this.state.disabled || this.props.disabled}
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
>
|
||||||
|
{this.props.children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
35
shared/js/ui/react-elements/Icon.tsx
Normal file
35
shared/js/ui/react-elements/Icon.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface IconProperties {
|
||||||
|
icon: string | JQuery<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IconRenderer extends React.Component<IconProperties, {}> {
|
||||||
|
private readonly icon_ref: React.RefObject<HTMLDivElement>;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
if(typeof this.props.icon === "object")
|
||||||
|
this.icon_ref = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if(!this.props.icon)
|
||||||
|
return <div className={"icon-container icon-empty"} />;
|
||||||
|
else if(typeof this.props.icon === "string")
|
||||||
|
return <div className={"icon " + this.props.icon} />;
|
||||||
|
|
||||||
|
|
||||||
|
return <div ref={this.icon_ref} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
if(this.icon_ref)
|
||||||
|
$(this.icon_ref.current).replaceWith(this.props.icon);
|
||||||
|
}
|
||||||
|
componentWillUnmount(): void {
|
||||||
|
if(this.icon_ref)
|
||||||
|
$(this.icon_ref.current).empty();
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,10 +11,12 @@ export abstract class ReactComponentBase<Properties, State> extends React.Compon
|
||||||
protected initialize() { }
|
protected initialize() { }
|
||||||
protected abstract default_state() : State;
|
protected abstract default_state() : State;
|
||||||
|
|
||||||
updateState(updates: {[key in keyof State]?: State[key]}) {
|
updateState(updates: {[key in keyof State]?: State[key]}, callback?: () => void) {
|
||||||
if(Object.keys(updates).findIndex(e => updates[e] !== this.state[e]) === -1)
|
if(Object.keys(updates).findIndex(e => updates[e] !== this.state[e]) === -1) {
|
||||||
|
if(callback) setTimeout(callback, 0);
|
||||||
return; /* no state has been changed */
|
return; /* no state has been changed */
|
||||||
this.setState(Object.assign(this.state, updates));
|
}
|
||||||
|
this.setState(Object.assign(this.state, updates), callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected classList(...classes: (string | undefined)[]) {
|
protected classList(...classes: (string | undefined)[]) {
|
|
@ -9,6 +9,6 @@ export class Translatable extends React.Component<{ message: string }, { transla
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return this.state.translated;
|
return this.state.translated || "";
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Add table
Reference in a new issue