Adding a channel popout/popin button for the channel popout renderer
This commit is contained in:
parent
ee4da7fbcc
commit
9ab430e8db
15 changed files with 236 additions and 102 deletions
|
@ -50,24 +50,6 @@ export default class implements ApplicationLoader {
|
||||||
container.setAttribute('id', "sounds");
|
container.setAttribute('id', "sounds");
|
||||||
body.append(container);
|
body.append(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* mouse move container */
|
|
||||||
{
|
|
||||||
const container = document.createElement("div");
|
|
||||||
container.setAttribute('id', "mouse-move");
|
|
||||||
|
|
||||||
body.append(container);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* tooltip container */
|
|
||||||
{
|
|
||||||
const container = document.createElement("div");
|
|
||||||
container.setAttribute('id', "global-tooltip");
|
|
||||||
|
|
||||||
container.append(document.createElement("a"));
|
|
||||||
|
|
||||||
body.append(container);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
priority: 10
|
priority: 10
|
||||||
});
|
});
|
||||||
|
|
|
@ -26,6 +26,7 @@ import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
||||||
import {tra} from "tc-shared/i18n/localize";
|
import {tra} from "tc-shared/i18n/localize";
|
||||||
import {EventType} from "tc-shared/ui/frames/log/Definitions";
|
import {EventType} from "tc-shared/ui/frames/log/Definitions";
|
||||||
import {renderChannelTree} from "tc-shared/ui/tree/Controller";
|
import {renderChannelTree} from "tc-shared/ui/tree/Controller";
|
||||||
|
import {ChannelTreePopoutController} from "tc-shared/ui/tree/popout/Controller";
|
||||||
|
|
||||||
export interface ChannelTreeEvents {
|
export interface ChannelTreeEvents {
|
||||||
action_select_entries: {
|
action_select_entries: {
|
||||||
|
@ -43,6 +44,7 @@ export interface ChannelTreeEvents {
|
||||||
notify_tree_reset: {},
|
notify_tree_reset: {},
|
||||||
notify_selection_changed: {},
|
notify_selection_changed: {},
|
||||||
notify_query_view_state_changed: { queries_shown: boolean },
|
notify_query_view_state_changed: { queries_shown: boolean },
|
||||||
|
notify_popout_state_changed: { popoutShown: boolean },
|
||||||
|
|
||||||
notify_entry_move_begin: {},
|
notify_entry_move_begin: {},
|
||||||
notify_entry_move_end: {},
|
notify_entry_move_end: {},
|
||||||
|
@ -231,19 +233,18 @@ export class ChannelTree {
|
||||||
/* whatever all channels have been initiaized */
|
/* whatever all channels have been initiaized */
|
||||||
channelsInitialized: boolean = false;
|
channelsInitialized: boolean = false;
|
||||||
|
|
||||||
//readonly view: React.RefObject<ChannelTreeView>;
|
|
||||||
//readonly view_move: React.RefObject<TreeEntryMove>;
|
|
||||||
readonly selection: ChannelTreeEntrySelect;
|
readonly selection: ChannelTreeEntrySelect;
|
||||||
|
readonly popoutController: ChannelTreePopoutController;
|
||||||
|
|
||||||
private readonly _tag_container: JQuery;
|
private readonly tagContainer: JQuery;
|
||||||
|
|
||||||
private _show_queries: boolean;
|
private _show_queries: boolean;
|
||||||
private channel_last?: ChannelEntry;
|
private channel_last?: ChannelEntry;
|
||||||
private channel_first?: ChannelEntry;
|
private channel_first?: ChannelEntry;
|
||||||
|
|
||||||
private _tag_container_focused = false;
|
private tagContainerFocused = false;
|
||||||
private _listener_document_click;
|
private listenerDocumentClick;
|
||||||
private _listener_document_key;
|
private listenerDocumentKeyPress;
|
||||||
|
|
||||||
constructor(client) {
|
constructor(client) {
|
||||||
this.events = new Registry<ChannelTreeEvents>();
|
this.events = new Registry<ChannelTreeEvents>();
|
||||||
|
@ -253,21 +254,16 @@ export class ChannelTree {
|
||||||
|
|
||||||
this.server = new ServerEntry(this, "undefined", undefined);
|
this.server = new ServerEntry(this, "undefined", undefined);
|
||||||
this.selection = new ChannelTreeEntrySelect(this);
|
this.selection = new ChannelTreeEntrySelect(this);
|
||||||
|
this.popoutController = new ChannelTreePopoutController(this);
|
||||||
|
|
||||||
this._tag_container = $.spawn("div").addClass("channel-tree-container");
|
this.tagContainer = $.spawn("div").addClass("channel-tree-container");
|
||||||
renderChannelTree(this, this._tag_container[0]);
|
renderChannelTree(this, this.tagContainer[0], { popoutButton: true });
|
||||||
/*
|
|
||||||
ReactDOM.render([
|
|
||||||
<ChannelTreeView key={"tree"} onMoveStart={(a,b) => this.onChannelEntryMove(a, b)} tree={this} ref={this.view} />,
|
|
||||||
<TreeEntryMove key={"move"} onMoveEnd={(point) => this.onMoveEnd(point.x, point.y)} ref={this.view_move} />
|
|
||||||
], this._tag_container[0]);
|
|
||||||
*/
|
|
||||||
|
|
||||||
this.reset();
|
this.reset();
|
||||||
|
|
||||||
if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) {
|
if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) {
|
||||||
/*
|
/*
|
||||||
TODO: Move this into the channel tree renderer
|
TODO: Show the context menu when clicked on no channel
|
||||||
this._tag_container.on("contextmenu", (event) => {
|
this._tag_container.on("contextmenu", (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
@ -284,24 +280,25 @@ export class ChannelTree {
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
this._listener_document_key = event => this.handle_key_press(event);
|
/* FIXME: Move this to the channel tree renderer */
|
||||||
this._listener_document_click = event => {
|
this.listenerDocumentKeyPress = event => this.handle_key_press(event);
|
||||||
this._tag_container_focused = false;
|
this.listenerDocumentClick = event => {
|
||||||
|
this.tagContainerFocused = false;
|
||||||
let element = event.target as HTMLElement;
|
let element = event.target as HTMLElement;
|
||||||
while(element) {
|
while(element) {
|
||||||
if(element === this._tag_container[0]) {
|
if(element === this.tagContainer[0]) {
|
||||||
this._tag_container_focused = true;
|
this.tagContainerFocused = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
element = element.parentNode as HTMLElement;
|
element = element.parentNode as HTMLElement;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener('click', this._listener_document_click);
|
document.addEventListener('click', this.listenerDocumentClick);
|
||||||
document.addEventListener('keydown', this._listener_document_key);
|
document.addEventListener('keydown', this.listenerDocumentKeyPress);
|
||||||
}
|
}
|
||||||
|
|
||||||
tag_tree() : JQuery {
|
tag_tree() : JQuery {
|
||||||
return this._tag_container;
|
return this.tagContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
channelsOrdered() : ChannelEntry[] {
|
channelsOrdered() : ChannelEntry[] {
|
||||||
|
@ -337,13 +334,13 @@ export class ChannelTree {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
ReactDOM.unmountComponentAtNode(this._tag_container[0]);
|
ReactDOM.unmountComponentAtNode(this.tagContainer[0]);
|
||||||
|
|
||||||
this._listener_document_click && document.removeEventListener('click', this._listener_document_click);
|
this.listenerDocumentClick && document.removeEventListener('click', this.listenerDocumentClick);
|
||||||
this._listener_document_click = undefined;
|
this.listenerDocumentClick = undefined;
|
||||||
|
|
||||||
this._listener_document_key && document.removeEventListener('keydown', this._listener_document_key);
|
this.listenerDocumentKeyPress && document.removeEventListener('keydown', this.listenerDocumentKeyPress);
|
||||||
this._listener_document_key = undefined;
|
this.listenerDocumentKeyPress = undefined;
|
||||||
|
|
||||||
if(this.server) {
|
if(this.server) {
|
||||||
this.server.destroy();
|
this.server.destroy();
|
||||||
|
@ -354,7 +351,8 @@ export class ChannelTree {
|
||||||
this.channel_first = undefined;
|
this.channel_first = undefined;
|
||||||
this.channel_last = undefined;
|
this.channel_last = undefined;
|
||||||
|
|
||||||
this._tag_container.remove();
|
this.popoutController.destroy();
|
||||||
|
this.tagContainer.remove();
|
||||||
this.selection.destroy();
|
this.selection.destroy();
|
||||||
this.events.destroy();
|
this.events.destroy();
|
||||||
}
|
}
|
||||||
|
@ -992,7 +990,7 @@ export class ChannelTree {
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_key_press(event: KeyboardEvent) {
|
handle_key_press(event: KeyboardEvent) {
|
||||||
if(!this._tag_container_focused || !this.selection.is_anything_selected() || this.selection.is_multi_select()) return;
|
if(!this.tagContainerFocused || !this.selection.is_anything_selected() || this.selection.is_multi_select()) return;
|
||||||
|
|
||||||
const selected = this.selection.selected_entries[0];
|
const selected = this.selection.selected_entries[0];
|
||||||
if(event.keyCode == KeyCode.KEY_UP) {
|
if(event.keyCode == KeyCode.KEY_UP) {
|
||||||
|
|
|
@ -301,7 +301,7 @@ const QueryButton = () => {
|
||||||
>
|
>
|
||||||
{toggle}
|
{toggle}
|
||||||
<DropdownEntry icon={ClientIcon.ServerQuery} text={<Translatable>Manage server queries</Translatable>}
|
<DropdownEntry icon={ClientIcon.ServerQuery} text={<Translatable>Manage server queries</Translatable>}
|
||||||
onClick={() => events.fire("action_query_manage")}/>
|
onClick={() => events.fire("action_query_manage")} key={"manage-entries"} />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -347,16 +347,16 @@ export const ControlBar2 = (props: { events: Registry<ControlBarEvents>, classNa
|
||||||
|
|
||||||
if(mode !== "channel-popout") {
|
if(mode !== "channel-popout") {
|
||||||
items.push(<ConnectButton key={"connect"} />);
|
items.push(<ConnectButton key={"connect"} />);
|
||||||
}
|
|
||||||
items.push(<BookmarkButton key={"bookmarks"} />);
|
items.push(<BookmarkButton key={"bookmarks"} />);
|
||||||
items.push(<div className={cssStyle.divider + " " + cssStyle.hideSmallPopout} />);
|
items.push(<div className={cssStyle.divider + " " + cssStyle.hideSmallPopout} key={"divider-1"} />);
|
||||||
|
}
|
||||||
items.push(<AwayButton key={"away"} />);
|
items.push(<AwayButton key={"away"} />);
|
||||||
items.push(<MicrophoneButton key={"microphone"} />);
|
items.push(<MicrophoneButton key={"microphone"} />);
|
||||||
items.push(<SpeakerButton key={"speaker"} />);
|
items.push(<SpeakerButton key={"speaker"} />);
|
||||||
items.push(<div className={cssStyle.divider + " " + cssStyle.hideSmallPopout} />);
|
items.push(<div className={cssStyle.divider + " " + cssStyle.hideSmallPopout} key={"divider-2"} />);
|
||||||
items.push(<SubscribeButton key={"subscribe"} />);
|
items.push(<SubscribeButton key={"subscribe"} />);
|
||||||
items.push(<QueryButton key={"query"} />);
|
items.push(<QueryButton key={"query"} />);
|
||||||
items.push(<div className={cssStyle.spacer} />);
|
items.push(<div className={cssStyle.spacer} key={"spacer"} />);
|
||||||
items.push(<HostButton key={"hostbutton"} />);
|
items.push(<HostButton key={"hostbutton"} />);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
5
shared/js/ui/react-elements/Icon.scss
Normal file
5
shared/js/ui/react-elements/Icon.scss
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.empty {
|
||||||
|
/* legacy values, we're using em now */
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
|
@ -2,13 +2,15 @@ import * as React from "react";
|
||||||
import {RemoteIcon} from "tc-shared/file/Icons";
|
import {RemoteIcon} from "tc-shared/file/Icons";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
|
|
||||||
|
const cssStyle = require("./Icon.scss");
|
||||||
|
|
||||||
export const IconRenderer = (props: {
|
export const IconRenderer = (props: {
|
||||||
icon: string;
|
icon: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) => {
|
}) => {
|
||||||
if(!props.icon) {
|
if(!props.icon) {
|
||||||
return <div className={"icon-container icon-empty " + props.className} title={props.title} />;
|
return <div className={cssStyle.empty + " icon-container icon-empty " + props.className} title={props.title} />;
|
||||||
} else if(typeof props.icon === "string") {
|
} else if(typeof props.icon === "string") {
|
||||||
return <div className={"icon " + props.icon + " " + props.className} title={props.title} />;
|
return <div className={"icon " + props.icon + " " + props.className} title={props.title} />;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -40,7 +40,7 @@ export abstract class AbstractModal {
|
||||||
protected constructor() {}
|
protected constructor() {}
|
||||||
|
|
||||||
abstract renderBody() : ReactElement;
|
abstract renderBody() : ReactElement;
|
||||||
abstract title() : string | React.ReactElement<Translatable>;
|
abstract title() : string | React.ReactElement;
|
||||||
|
|
||||||
/* only valid for the "inline" modals */
|
/* only valid for the "inline" modals */
|
||||||
type() : ModalType { return "none"; }
|
type() : ModalType { return "none"; }
|
||||||
|
|
|
@ -108,8 +108,9 @@ export abstract class AbstractExternalModalController extends EventControllerBas
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.doDestroyWindow();
|
this.doDestroyWindow();
|
||||||
if(this.ipcChannel)
|
if(this.ipcChannel) {
|
||||||
ipc.getInstance().deleteChannel(this.ipcChannel);
|
ipc.getInstance().deleteChannel(this.ipcChannel);
|
||||||
|
}
|
||||||
|
|
||||||
this.destroyIPC();
|
this.destroyIPC();
|
||||||
this.modalState = ModalState.DESTROYED;
|
this.modalState = ModalState.DESTROYED;
|
||||||
|
@ -117,6 +118,7 @@ export abstract class AbstractExternalModalController extends EventControllerBas
|
||||||
}
|
}
|
||||||
|
|
||||||
protected handleWindowClosed() {
|
protected handleWindowClosed() {
|
||||||
|
/* no other way currently */
|
||||||
this.destroy();
|
this.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,13 +19,16 @@ import {VoiceConnectionEvents, VoiceConnectionStatus} from "tc-shared/connection
|
||||||
import {spawnFileTransferModal} from "tc-shared/ui/modal/transfer/ModalFileTransfer";
|
import {spawnFileTransferModal} from "tc-shared/ui/modal/transfer/ModalFileTransfer";
|
||||||
import {GroupManager, GroupManagerEvents} from "tc-shared/permission/GroupManager";
|
import {GroupManager, GroupManagerEvents} from "tc-shared/permission/GroupManager";
|
||||||
import {ServerEntry} from "tc-shared/tree/Server";
|
import {ServerEntry} from "tc-shared/tree/Server";
|
||||||
import {spawnChannelTreePopout} from "tc-shared/ui/tree/popout/Controller";
|
|
||||||
import {server_connections} from "tc-shared/ConnectionManager";
|
import {server_connections} from "tc-shared/ConnectionManager";
|
||||||
|
|
||||||
export function renderChannelTree(channelTree: ChannelTree, target: HTMLElement) {
|
export interface ChannelTreeRendererOptions {
|
||||||
|
popoutButton: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderChannelTree(channelTree: ChannelTree, target: HTMLElement, options: ChannelTreeRendererOptions) {
|
||||||
const events = new Registry<ChannelTreeUIEvents>();
|
const events = new Registry<ChannelTreeUIEvents>();
|
||||||
events.enableDebug("channel-tree-view");
|
events.enableDebug("channel-tree-view");
|
||||||
initializeChannelTreeController(events, channelTree);
|
initializeChannelTreeController(events, channelTree, options);
|
||||||
|
|
||||||
ReactDOM.render(<ChannelTreeRenderer handlerId={channelTree.client.handlerId} events={events} />, target);
|
ReactDOM.render(<ChannelTreeRenderer handlerId={channelTree.client.handlerId} events={events} />, target);
|
||||||
|
|
||||||
|
@ -40,10 +43,6 @@ export function renderChannelTree(channelTree: ChannelTree, target: HTMLElement)
|
||||||
events.fire("notify_destroy");
|
events.fire("notify_destroy");
|
||||||
events.destroy();
|
events.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
(window as any).chan_pop = () => {
|
|
||||||
spawnChannelTreePopout(channelTree.client);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* FIXME: Client move is not a part of the channel tree, it's part of our own controller here */
|
/* FIXME: Client move is not a part of the channel tree, it's part of our own controller here */
|
||||||
|
@ -86,6 +85,7 @@ const ClientTalkStatusUpdateKeys: (keyof ClientProperties)[] = [
|
||||||
class ChannelTreeController {
|
class ChannelTreeController {
|
||||||
readonly events: Registry<ChannelTreeUIEvents>;
|
readonly events: Registry<ChannelTreeUIEvents>;
|
||||||
readonly channelTree: ChannelTree;
|
readonly channelTree: ChannelTree;
|
||||||
|
readonly options: ChannelTreeRendererOptions;
|
||||||
|
|
||||||
/* the key here is the unique entry id! */
|
/* the key here is the unique entry id! */
|
||||||
private eventListeners: {[key: number]: (() => void)[]} = {};
|
private eventListeners: {[key: number]: (() => void)[]} = {};
|
||||||
|
@ -96,9 +96,10 @@ class ChannelTreeController {
|
||||||
private readonly groupUpdatedListener;
|
private readonly groupUpdatedListener;
|
||||||
private readonly groupsReceivedListener;
|
private readonly groupsReceivedListener;
|
||||||
|
|
||||||
constructor(events, channelTree) {
|
constructor(events, channelTree, options: ChannelTreeRendererOptions) {
|
||||||
this.events = events;
|
this.events = events;
|
||||||
this.channelTree = channelTree;
|
this.channelTree = channelTree;
|
||||||
|
this.options = options;
|
||||||
|
|
||||||
this.connectionStateListener = this.handleConnectionStateChanged.bind(this);
|
this.connectionStateListener = this.handleConnectionStateChanged.bind(this);
|
||||||
this.voiceConnectionStateListener = this.handleVoiceConnectionStateChanged.bind(this);
|
this.voiceConnectionStateListener = this.handleVoiceConnectionStateChanged.bind(this);
|
||||||
|
@ -184,6 +185,11 @@ class ChannelTreeController {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* general channel tree event handlers */
|
/* general channel tree event handlers */
|
||||||
|
@EventHandler<ChannelTreeEvents>("notify_popout_state_changed")
|
||||||
|
private handlePoputStateChanged() {
|
||||||
|
this.sendPopoutState();
|
||||||
|
}
|
||||||
|
|
||||||
@EventHandler<ChannelTreeEvents>("notify_channel_list_received")
|
@EventHandler<ChannelTreeEvents>("notify_channel_list_received")
|
||||||
private handleChannelListReceived() {
|
private handleChannelListReceived() {
|
||||||
this.channelTreeInitialized = true;
|
this.channelTreeInitialized = true;
|
||||||
|
@ -338,6 +344,13 @@ class ChannelTreeController {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* notify state update methods */
|
/* notify state update methods */
|
||||||
|
public sendPopoutState() {
|
||||||
|
this.events.fire_async("notify_popout_state", {
|
||||||
|
showButton: this.options.popoutButton,
|
||||||
|
shown: this.channelTree.popoutController.hasBeenPopedOut()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public sendChannelTreeEntries() {
|
public sendChannelTreeEntries() {
|
||||||
const entries = [] as ChannelTreeEntry[];
|
const entries = [] as ChannelTreeEntry[];
|
||||||
|
|
||||||
|
@ -520,13 +533,14 @@ class ChannelTreeController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initializeChannelTreeController(events: Registry<ChannelTreeUIEvents>, channelTree: ChannelTree) {
|
export function initializeChannelTreeController(events: Registry<ChannelTreeUIEvents>, channelTree: ChannelTree, options: ChannelTreeRendererOptions) {
|
||||||
/* initialize the general update handler */
|
/* initialize the general update handler */
|
||||||
const controller = new ChannelTreeController(events, channelTree);
|
const controller = new ChannelTreeController(events, channelTree, options);
|
||||||
controller.initialize();
|
controller.initialize();
|
||||||
events.on("notify_destroy", () => controller.destroy());
|
events.on("notify_destroy", () => controller.destroy());
|
||||||
|
|
||||||
/* initialize the query handlers */
|
/* initialize the query handlers */
|
||||||
|
events.on("query_popout_state", () => controller.sendPopoutState());
|
||||||
|
|
||||||
events.on("query_unread_state", event => {
|
events.on("query_unread_state", event => {
|
||||||
const entry = channelTree.findEntryId(event.treeEntryId);
|
const entry = channelTree.findEntryId(event.treeEntryId);
|
||||||
|
@ -624,6 +638,14 @@ export function initializeChannelTreeController(events: Registry<ChannelTreeUIEv
|
||||||
controller.sendServerStatus(entry);
|
controller.sendServerStatus(entry);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
events.on("action_toggle_popout", event => {
|
||||||
|
if(event.shown) {
|
||||||
|
channelTree.popoutController.popout();
|
||||||
|
} else {
|
||||||
|
channelTree.popoutController.popin();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
events.on("action_set_collapsed_state", event => {
|
events.on("action_set_collapsed_state", event => {
|
||||||
const entry = channelTree.findEntryId(event.treeEntryId);
|
const entry = channelTree.findEntryId(event.treeEntryId);
|
||||||
if(!entry || !(entry instanceof ChannelEntry)) {
|
if(!entry || !(entry instanceof ChannelEntry)) {
|
||||||
|
|
|
@ -28,6 +28,7 @@ export type ServerState = { state: "disconnected" } | { state: "connecting", tar
|
||||||
|
|
||||||
export interface ChannelTreeUIEvents {
|
export interface ChannelTreeUIEvents {
|
||||||
/* actions */
|
/* actions */
|
||||||
|
action_toggle_popout: { shown: boolean },
|
||||||
action_show_context_menu: { treeEntryId: number, pageX: number, pageY: number },
|
action_show_context_menu: { treeEntryId: number, pageX: number, pageY: number },
|
||||||
action_start_entry_move: { start: { x: number, y: number }, current: { x: number, y: number } },
|
action_start_entry_move: { start: { x: number, y: number }, current: { x: number, y: number } },
|
||||||
action_set_collapsed_state: { treeEntryId: number, state: "collapsed" | "expended" },
|
action_set_collapsed_state: { treeEntryId: number, state: "collapsed" | "expended" },
|
||||||
|
@ -44,6 +45,8 @@ export interface ChannelTreeUIEvents {
|
||||||
|
|
||||||
/* queries */
|
/* queries */
|
||||||
query_tree_entries: {},
|
query_tree_entries: {},
|
||||||
|
query_popout_state: {},
|
||||||
|
|
||||||
query_unread_state: { treeEntryId: number },
|
query_unread_state: { treeEntryId: number },
|
||||||
query_select_state: { treeEntryId: number },
|
query_select_state: { treeEntryId: number },
|
||||||
|
|
||||||
|
@ -60,6 +63,7 @@ export interface ChannelTreeUIEvents {
|
||||||
|
|
||||||
/* notifies */
|
/* notifies */
|
||||||
notify_tree_entries: { entries: ChannelTreeEntry[] },
|
notify_tree_entries: { entries: ChannelTreeEntry[] },
|
||||||
|
notify_popout_state: { shown: boolean, showButton: boolean },
|
||||||
|
|
||||||
notify_channel_info: { treeEntryId: number, info: ChannelEntryInfo },
|
notify_channel_info: { treeEntryId: number, info: ChannelEntryInfo },
|
||||||
notify_channel_icon: { treeEntryId: number, icon: ClientIcon },
|
notify_channel_icon: { treeEntryId: number, icon: ClientIcon },
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
ClientIcons,
|
ClientIcons,
|
||||||
ClientNameInfo, ClientTalkIconState, ServerState
|
ClientNameInfo, ClientTalkIconState, ServerState
|
||||||
} from "tc-shared/ui/tree/Definitions";
|
} from "tc-shared/ui/tree/Definitions";
|
||||||
import {ChannelTreeView} from "tc-shared/ui/tree/RendererView";
|
import {ChannelTreeView, PopoutButton} from "tc-shared/ui/tree/RendererView";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {ChannelIconClass, ChannelIconsRenderer, RendererChannel} from "tc-shared/ui/tree/RendererChannel";
|
import {ChannelIconClass, ChannelIconsRenderer, RendererChannel} from "tc-shared/ui/tree/RendererChannel";
|
||||||
import {ClientIcon} from "svg-sprites/client-icons";
|
import {ClientIcon} from "svg-sprites/client-icons";
|
||||||
|
@ -69,6 +69,10 @@ export class RDPChannelTree {
|
||||||
|
|
||||||
readonly refMove = React.createRef<RendererMove>();
|
readonly refMove = React.createRef<RendererMove>();
|
||||||
readonly refTree = React.createRef<ChannelTreeView>();
|
readonly refTree = React.createRef<ChannelTreeView>();
|
||||||
|
readonly refPopoutButton = React.createRef<PopoutButton>();
|
||||||
|
|
||||||
|
popoutShown: boolean = false;
|
||||||
|
popoutButtonShown: boolean = false;
|
||||||
|
|
||||||
private treeRevision: number = 0;
|
private treeRevision: number = 0;
|
||||||
private orderedTree: RDPEntry[] = [];
|
private orderedTree: RDPEntry[] = [];
|
||||||
|
@ -198,6 +202,7 @@ export class RDPChannelTree {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.events.fire("query_tree_entries");
|
this.events.fire("query_tree_entries");
|
||||||
|
this.events.fire("query_popout_state");
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
@ -268,6 +273,13 @@ export class RDPChannelTree {
|
||||||
|
|
||||||
this.refMove.current.enableEntryMove(event.entries, event.begin, event.current);
|
this.refMove.current.enableEntryMove(event.entries, event.begin, event.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EventHandler<ChannelTreeUIEvents>("notify_popout_state")
|
||||||
|
private handleNotifyPopoutState(event: ChannelTreeUIEvents["notify_popout_state"]) {
|
||||||
|
this.popoutShown = event.shown;
|
||||||
|
this.popoutButtonShown = event.showButton;
|
||||||
|
this.refPopoutButton.current?.forceUpdate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class RDPEntry {
|
export abstract class RDPEntry {
|
||||||
|
|
|
@ -14,15 +14,21 @@ import {ClientIcon} from "svg-sprites/client-icons";
|
||||||
|
|
||||||
const viewStyle = require("./View.scss");
|
const viewStyle = require("./View.scss");
|
||||||
|
|
||||||
const PopoutButton = (props: {}) => {
|
export class PopoutButton extends React.Component<{ tree: RDPChannelTree }, {}> {
|
||||||
|
render() {
|
||||||
|
if(!this.props.tree.popoutButtonShown) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={viewStyle.popoutButton}>
|
<div className={viewStyle.popoutButton} onClick={() => this.props.tree.events.fire("action_toggle_popout", { shown: !this.props.tree.popoutShown })}>
|
||||||
<div className={viewStyle.button}>
|
<div className={viewStyle.button} title={this.props.tree.popoutShown ? tr("Popin the second channel tree view") : tr("Popout the channel tree view")}>
|
||||||
<ClientIconRenderer icon={ClientIcon.ChannelPopout} />
|
<ClientIconRenderer icon={this.props.tree.popoutShown ? ClientIcon.ChannelPopin : ClientIcon.ChannelPopout} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface ChannelTreeViewProperties {
|
export interface ChannelTreeViewProperties {
|
||||||
events: Registry<ChannelTreeUIEvents>;
|
events: Registry<ChannelTreeUIEvents>;
|
||||||
|
@ -203,7 +209,7 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
|
||||||
ref={this.props.dataProvider.refMove}
|
ref={this.props.dataProvider.refMove}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<PopoutButton />
|
<PopoutButton tree={this.props.dataProvider} ref={this.props.dataProvider.refPopoutButton} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
|
||||||
import {Registry} from "tc-shared/events";
|
import {Registry} from "tc-shared/events";
|
||||||
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
|
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
|
||||||
import {spawnExternalModal} from "tc-shared/ui/react-elements/external-modal";
|
import {spawnExternalModal} from "tc-shared/ui/react-elements/external-modal";
|
||||||
|
@ -7,35 +6,114 @@ import {ControlBarEvents} from "tc-shared/ui/frames/control-bar/Definitions";
|
||||||
import {
|
import {
|
||||||
initializePopoutControlBarController
|
initializePopoutControlBarController
|
||||||
} from "tc-shared/ui/frames/control-bar/Controller";
|
} from "tc-shared/ui/frames/control-bar/Controller";
|
||||||
import {server_connections} from "tc-shared/ConnectionManager";
|
import {ChannelTree} from "tc-shared/tree/ChannelTree";
|
||||||
|
import {ModalController} from "tc-shared/ui/react-elements/ModalDefinitions";
|
||||||
|
import {ChannelTreePopoutEvents} from "tc-shared/ui/tree/popout/Definitions";
|
||||||
|
import {ConnectionState} from "tc-shared/ConnectionHandler";
|
||||||
|
|
||||||
export function spawnChannelTreePopout(handler: ConnectionHandler) {
|
export class ChannelTreePopoutController {
|
||||||
const eventsTree = new Registry<ChannelTreeUIEvents>();
|
readonly channelTree: ChannelTree;
|
||||||
eventsTree.enableDebug("channel-tree-view-modal");
|
|
||||||
initializeChannelTreeController(eventsTree, handler.channelTree);
|
|
||||||
|
|
||||||
const eventsControlBar = new Registry<ControlBarEvents>();
|
private popoutInstance: ModalController;
|
||||||
initializePopoutControlBarController(eventsControlBar, handler);
|
private uiEvents: Registry<ChannelTreePopoutEvents>;
|
||||||
|
private treeEvents: Registry<ChannelTreeUIEvents>;
|
||||||
|
private controlBarEvents: Registry<ControlBarEvents>;
|
||||||
|
|
||||||
let handlerDestroyListener;
|
private generalEvents: (() => void)[];
|
||||||
server_connections.events().on("notify_handler_deleted", handlerDestroyListener = event => {
|
|
||||||
if(event.handler !== handler) {
|
constructor(channelTree: ChannelTree) {
|
||||||
|
this.channelTree = channelTree;
|
||||||
|
|
||||||
|
this.generalEvents = [];
|
||||||
|
this.generalEvents.push(this.channelTree.server.events.on("notify_properties_updated", event => {
|
||||||
|
if("virtualserver_name" in event.updated_properties) {
|
||||||
|
this.sendTitle();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.generalEvents.push(this.channelTree.client.events().on("notify_connection_state_changed", () => this.sendTitle()));
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.popin();
|
||||||
|
this.generalEvents?.forEach(callback => callback());
|
||||||
|
this.generalEvents = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasBeenPopedOut() {
|
||||||
|
return !!this.popoutInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
popout() {
|
||||||
|
if(this.popoutInstance) {
|
||||||
|
/* TODO: Request focus on that window? */
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
modal.destroy();
|
this.uiEvents = new Registry<ChannelTreePopoutEvents>();
|
||||||
|
this.uiEvents.on("query_title", () => this.sendTitle());
|
||||||
|
|
||||||
|
this.treeEvents = new Registry<ChannelTreeUIEvents>();
|
||||||
|
initializeChannelTreeController(this.treeEvents, this.channelTree, { popoutButton: false });
|
||||||
|
|
||||||
|
this.controlBarEvents = new Registry<ControlBarEvents>();
|
||||||
|
initializePopoutControlBarController(this.controlBarEvents, this.channelTree.client);
|
||||||
|
|
||||||
|
this.popoutInstance = spawnExternalModal("channel-tree", {
|
||||||
|
tree: this.treeEvents,
|
||||||
|
controlBar: this.controlBarEvents,
|
||||||
|
base: this.uiEvents
|
||||||
|
}, { handlerId: this.channelTree.client.handlerId }, "channel-tree-" + this.channelTree.client.handlerId);
|
||||||
|
|
||||||
|
this.popoutInstance.getEvents().one("destroy", () => {
|
||||||
|
this.treeEvents.fire("notify_destroy");
|
||||||
|
this.treeEvents.destroy();
|
||||||
|
this.treeEvents = undefined;
|
||||||
|
|
||||||
|
this.controlBarEvents.fire("notify_destroy");
|
||||||
|
this.controlBarEvents.destroy();
|
||||||
|
this.controlBarEvents = undefined;
|
||||||
|
|
||||||
|
this.uiEvents.destroy();
|
||||||
|
this.uiEvents = undefined;
|
||||||
|
|
||||||
|
this.popoutInstance = undefined;
|
||||||
|
this.channelTree.events.fire("notify_popout_state_changed", { popoutShown: false });
|
||||||
});
|
});
|
||||||
|
this.popoutInstance.show();
|
||||||
|
|
||||||
const modal = spawnExternalModal("channel-tree", { tree: eventsTree, controlBar: eventsControlBar }, { handlerId: handler.handlerId }, "channel-tree-" + handler.handlerId);
|
this.channelTree.events.fire("notify_popout_state_changed", { popoutShown: true });
|
||||||
modal.show();
|
}
|
||||||
|
|
||||||
modal.getEvents().on("destroy", () => {
|
popin() {
|
||||||
server_connections.events().off("notify_handler_deleted", handlerDestroyListener);
|
if(!this.popoutInstance) { return; }
|
||||||
|
|
||||||
eventsTree.fire("notify_destroy");
|
this.popoutInstance.destroy();
|
||||||
eventsTree.destroy();
|
this.popoutInstance = undefined; /* not needed, but just to ensure (will be set within the destroy callback already) */
|
||||||
|
}
|
||||||
|
|
||||||
eventsControlBar.fire("notify_destroy");
|
private sendTitle() {
|
||||||
eventsControlBar.destroy();
|
if(!this.uiEvents) { return; }
|
||||||
});
|
|
||||||
|
let title;
|
||||||
|
switch (this.channelTree.client.connection_state) {
|
||||||
|
case ConnectionState.INITIALISING:
|
||||||
|
case ConnectionState.CONNECTING:
|
||||||
|
case ConnectionState.AUTHENTICATING:
|
||||||
|
const address = this.channelTree.server.remote_address;
|
||||||
|
title = tra("Connecting to {}", address.host + (address.port === 9987 ? "" : `:${address.port}`));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ConnectionState.DISCONNECTING:
|
||||||
|
case ConnectionState.UNCONNECTED:
|
||||||
|
title = tr("Not connected");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ConnectionState.CONNECTED:
|
||||||
|
title = this.channelTree.server.properties.virtualserver_name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.uiEvents.fire_async("notify_title", { title: title });
|
||||||
|
}
|
||||||
}
|
}
|
4
shared/js/ui/tree/popout/Definitions.ts
Normal file
4
shared/js/ui/tree/popout/Definitions.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export interface ChannelTreePopoutEvents {
|
||||||
|
query_title: {},
|
||||||
|
notify_title: { title: string }
|
||||||
|
}
|
|
@ -2,13 +2,25 @@ import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions";
|
||||||
import {Registry, RegistryMap} from "tc-shared/events";
|
import {Registry, RegistryMap} from "tc-shared/events";
|
||||||
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
|
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
|
||||||
import {ChannelTreeRenderer} from "tc-shared/ui/tree/Renderer";
|
import {ChannelTreeRenderer} from "tc-shared/ui/tree/Renderer";
|
||||||
import {ControlBarEvents} from "tc-shared/ui/frames/control-bar/Definitions";
|
import {ControlBarEvents} from "tc-shared/ui/frames/control-bar/Definitions";
|
||||||
import {ControlBar2} from "tc-shared/ui/frames/control-bar/Renderer";
|
import {ControlBar2} from "tc-shared/ui/frames/control-bar/Renderer";
|
||||||
|
import {ChannelTreePopoutEvents} from "tc-shared/ui/tree/popout/Definitions";
|
||||||
|
import {useState} from "react";
|
||||||
|
|
||||||
|
const TitleRenderer = (props: { events: Registry<ChannelTreePopoutEvents> }) => {
|
||||||
|
const [ title, setTitle ] = useState<string>(() => {
|
||||||
|
props.events.fire("query_title");
|
||||||
|
return tr("Channel tree popout");
|
||||||
|
});
|
||||||
|
|
||||||
|
props.events.reactUse("notify_title", event => setTitle(event.title));
|
||||||
|
return <>{title}</>;
|
||||||
|
}
|
||||||
|
|
||||||
const cssStyle = require("./RendererModal.scss");
|
const cssStyle = require("./RendererModal.scss");
|
||||||
class ChannelTreeModal extends AbstractModal {
|
class ChannelTreeModal extends AbstractModal {
|
||||||
|
readonly eventsUI: Registry<ChannelTreePopoutEvents>;
|
||||||
readonly eventsTree: Registry<ChannelTreeUIEvents>;
|
readonly eventsTree: Registry<ChannelTreeUIEvents>;
|
||||||
readonly eventsControlBar: Registry<ControlBarEvents>;
|
readonly eventsControlBar: Registry<ControlBarEvents>;
|
||||||
|
|
||||||
|
@ -18,8 +30,15 @@ class ChannelTreeModal extends AbstractModal {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.handlerId = userData.handlerId;
|
this.handlerId = userData.handlerId;
|
||||||
|
this.eventsUI = registryMap["base"] as any;
|
||||||
this.eventsTree = registryMap["tree"] as any;
|
this.eventsTree = registryMap["tree"] as any;
|
||||||
this.eventsControlBar = registryMap["controlBar"] as any;
|
this.eventsControlBar = registryMap["controlBar"] as any;
|
||||||
|
|
||||||
|
this.eventsUI.fire("query_title");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderBody(): React.ReactElement {
|
renderBody(): React.ReactElement {
|
||||||
|
@ -35,8 +54,8 @@ class ChannelTreeModal extends AbstractModal {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
title(): string | React.ReactElement<Translatable> {
|
title(): React.ReactElement {
|
||||||
return <Translatable>Channel tree</Translatable>;
|
return <TitleRenderer events={this.eventsUI} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -542,7 +542,7 @@ export class VoiceConnection extends AbstractVoiceConnection {
|
||||||
logWarn(LogCategory.CLIENT, tr("Failed to clear the whisper target: %o"), error);
|
logWarn(LogCategory.CLIENT, tr("Failed to clear the whisper target: %o"), error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.voiceBridge.stopWhispering();
|
this.voiceBridge?.stopWhispering();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue