Adding a channel popout/popin button for the channel popout renderer

canary
WolverinDEV 2020-09-29 15:02:36 +02:00
parent bf8c6ed857
commit 583cdd146e
15 changed files with 236 additions and 102 deletions

View File

@ -50,24 +50,6 @@ export default class implements ApplicationLoader {
container.setAttribute('id', "sounds");
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
});

View File

@ -26,6 +26,7 @@ import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
import {tra} from "tc-shared/i18n/localize";
import {EventType} from "tc-shared/ui/frames/log/Definitions";
import {renderChannelTree} from "tc-shared/ui/tree/Controller";
import {ChannelTreePopoutController} from "tc-shared/ui/tree/popout/Controller";
export interface ChannelTreeEvents {
action_select_entries: {
@ -43,6 +44,7 @@ export interface ChannelTreeEvents {
notify_tree_reset: {},
notify_selection_changed: {},
notify_query_view_state_changed: { queries_shown: boolean },
notify_popout_state_changed: { popoutShown: boolean },
notify_entry_move_begin: {},
notify_entry_move_end: {},
@ -231,19 +233,18 @@ export class ChannelTree {
/* whatever all channels have been initiaized */
channelsInitialized: boolean = false;
//readonly view: React.RefObject<ChannelTreeView>;
//readonly view_move: React.RefObject<TreeEntryMove>;
readonly selection: ChannelTreeEntrySelect;
readonly popoutController: ChannelTreePopoutController;
private readonly _tag_container: JQuery;
private readonly tagContainer: JQuery;
private _show_queries: boolean;
private channel_last?: ChannelEntry;
private channel_first?: ChannelEntry;
private _tag_container_focused = false;
private _listener_document_click;
private _listener_document_key;
private tagContainerFocused = false;
private listenerDocumentClick;
private listenerDocumentKeyPress;
constructor(client) {
this.events = new Registry<ChannelTreeEvents>();
@ -253,21 +254,16 @@ export class ChannelTree {
this.server = new ServerEntry(this, "undefined", undefined);
this.selection = new ChannelTreeEntrySelect(this);
this.popoutController = new ChannelTreePopoutController(this);
this._tag_container = $.spawn("div").addClass("channel-tree-container");
renderChannelTree(this, this._tag_container[0]);
/*
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.tagContainer = $.spawn("div").addClass("channel-tree-container");
renderChannelTree(this, this.tagContainer[0], { popoutButton: true });
this.reset();
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) => {
event.preventDefault();
@ -284,24 +280,25 @@ export class ChannelTree {
*/
}
this._listener_document_key = event => this.handle_key_press(event);
this._listener_document_click = event => {
this._tag_container_focused = false;
/* FIXME: Move this to the channel tree renderer */
this.listenerDocumentKeyPress = event => this.handle_key_press(event);
this.listenerDocumentClick = event => {
this.tagContainerFocused = false;
let element = event.target as HTMLElement;
while(element) {
if(element === this._tag_container[0]) {
this._tag_container_focused = true;
if(element === this.tagContainer[0]) {
this.tagContainerFocused = true;
break;
}
element = element.parentNode as HTMLElement;
}
};
document.addEventListener('click', this._listener_document_click);
document.addEventListener('keydown', this._listener_document_key);
document.addEventListener('click', this.listenerDocumentClick);
document.addEventListener('keydown', this.listenerDocumentKeyPress);
}
tag_tree() : JQuery {
return this._tag_container;
return this.tagContainer;
}
channelsOrdered() : ChannelEntry[] {
@ -337,13 +334,13 @@ export class ChannelTree {
}
destroy() {
ReactDOM.unmountComponentAtNode(this._tag_container[0]);
ReactDOM.unmountComponentAtNode(this.tagContainer[0]);
this._listener_document_click && document.removeEventListener('click', this._listener_document_click);
this._listener_document_click = undefined;
this.listenerDocumentClick && document.removeEventListener('click', this.listenerDocumentClick);
this.listenerDocumentClick = undefined;
this._listener_document_key && document.removeEventListener('keydown', this._listener_document_key);
this._listener_document_key = undefined;
this.listenerDocumentKeyPress && document.removeEventListener('keydown', this.listenerDocumentKeyPress);
this.listenerDocumentKeyPress = undefined;
if(this.server) {
this.server.destroy();
@ -354,7 +351,8 @@ export class ChannelTree {
this.channel_first = undefined;
this.channel_last = undefined;
this._tag_container.remove();
this.popoutController.destroy();
this.tagContainer.remove();
this.selection.destroy();
this.events.destroy();
}
@ -992,7 +990,7 @@ export class ChannelTree {
}
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];
if(event.keyCode == KeyCode.KEY_UP) {

View File

@ -301,7 +301,7 @@ const QueryButton = () => {
>
{toggle}
<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>
);
}
@ -347,16 +347,16 @@ export const ControlBar2 = (props: { events: Registry<ControlBarEvents>, classNa
if(mode !== "channel-popout") {
items.push(<ConnectButton key={"connect"} />);
items.push(<BookmarkButton key={"bookmarks"} />);
items.push(<div className={cssStyle.divider + " " + cssStyle.hideSmallPopout} key={"divider-1"} />);
}
items.push(<BookmarkButton key={"bookmarks"} />);
items.push(<div className={cssStyle.divider + " " + cssStyle.hideSmallPopout} />);
items.push(<AwayButton key={"away"} />);
items.push(<MicrophoneButton key={"microphone"} />);
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(<QueryButton key={"query"} />);
items.push(<div className={cssStyle.spacer} />);
items.push(<div className={cssStyle.spacer} key={"spacer"} />);
items.push(<HostButton key={"hostbutton"} />);
return (

View File

@ -0,0 +1,5 @@
.empty {
/* legacy values, we're using em now */
width: 16px;
height: 16px;
}

View File

@ -2,13 +2,15 @@ import * as React from "react";
import {RemoteIcon} from "tc-shared/file/Icons";
import {useState} from "react";
const cssStyle = require("./Icon.scss");
export const IconRenderer = (props: {
icon: string;
title?: string;
className?: string;
}) => {
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") {
return <div className={"icon " + props.icon + " " + props.className} title={props.title} />;
} else {

View File

@ -40,7 +40,7 @@ export abstract class AbstractModal {
protected constructor() {}
abstract renderBody() : ReactElement;
abstract title() : string | React.ReactElement<Translatable>;
abstract title() : string | React.ReactElement;
/* only valid for the "inline" modals */
type() : ModalType { return "none"; }

View File

@ -108,8 +108,9 @@ export abstract class AbstractExternalModalController extends EventControllerBas
return;
this.doDestroyWindow();
if(this.ipcChannel)
if(this.ipcChannel) {
ipc.getInstance().deleteChannel(this.ipcChannel);
}
this.destroyIPC();
this.modalState = ModalState.DESTROYED;
@ -117,6 +118,7 @@ export abstract class AbstractExternalModalController extends EventControllerBas
}
protected handleWindowClosed() {
/* no other way currently */
this.destroy();
}

View File

@ -19,13 +19,16 @@ import {VoiceConnectionEvents, VoiceConnectionStatus} from "tc-shared/connection
import {spawnFileTransferModal} from "tc-shared/ui/modal/transfer/ModalFileTransfer";
import {GroupManager, GroupManagerEvents} from "tc-shared/permission/GroupManager";
import {ServerEntry} from "tc-shared/tree/Server";
import {spawnChannelTreePopout} from "tc-shared/ui/tree/popout/Controller";
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>();
events.enableDebug("channel-tree-view");
initializeChannelTreeController(events, channelTree);
initializeChannelTreeController(events, channelTree, options);
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.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 */
@ -86,6 +85,7 @@ const ClientTalkStatusUpdateKeys: (keyof ClientProperties)[] = [
class ChannelTreeController {
readonly events: Registry<ChannelTreeUIEvents>;
readonly channelTree: ChannelTree;
readonly options: ChannelTreeRendererOptions;
/* the key here is the unique entry id! */
private eventListeners: {[key: number]: (() => void)[]} = {};
@ -96,9 +96,10 @@ class ChannelTreeController {
private readonly groupUpdatedListener;
private readonly groupsReceivedListener;
constructor(events, channelTree) {
constructor(events, channelTree, options: ChannelTreeRendererOptions) {
this.events = events;
this.channelTree = channelTree;
this.options = options;
this.connectionStateListener = this.handleConnectionStateChanged.bind(this);
this.voiceConnectionStateListener = this.handleVoiceConnectionStateChanged.bind(this);
@ -184,6 +185,11 @@ class ChannelTreeController {
}
/* general channel tree event handlers */
@EventHandler<ChannelTreeEvents>("notify_popout_state_changed")
private handlePoputStateChanged() {
this.sendPopoutState();
}
@EventHandler<ChannelTreeEvents>("notify_channel_list_received")
private handleChannelListReceived() {
this.channelTreeInitialized = true;
@ -338,6 +344,13 @@ class ChannelTreeController {
}
/* notify state update methods */
public sendPopoutState() {
this.events.fire_async("notify_popout_state", {
showButton: this.options.popoutButton,
shown: this.channelTree.popoutController.hasBeenPopedOut()
});
}
public sendChannelTreeEntries() {
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 */
const controller = new ChannelTreeController(events, channelTree);
const controller = new ChannelTreeController(events, channelTree, options);
controller.initialize();
events.on("notify_destroy", () => controller.destroy());
/* initialize the query handlers */
events.on("query_popout_state", () => controller.sendPopoutState());
events.on("query_unread_state", event => {
const entry = channelTree.findEntryId(event.treeEntryId);
@ -624,6 +638,14 @@ export function initializeChannelTreeController(events: Registry<ChannelTreeUIEv
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 => {
const entry = channelTree.findEntryId(event.treeEntryId);
if(!entry || !(entry instanceof ChannelEntry)) {

View File

@ -28,6 +28,7 @@ export type ServerState = { state: "disconnected" } | { state: "connecting", tar
export interface ChannelTreeUIEvents {
/* actions */
action_toggle_popout: { shown: boolean },
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_set_collapsed_state: { treeEntryId: number, state: "collapsed" | "expended" },
@ -44,6 +45,8 @@ export interface ChannelTreeUIEvents {
/* queries */
query_tree_entries: {},
query_popout_state: {},
query_unread_state: { treeEntryId: number },
query_select_state: { treeEntryId: number },
@ -60,6 +63,7 @@ export interface ChannelTreeUIEvents {
/* notifies */
notify_tree_entries: { entries: ChannelTreeEntry[] },
notify_popout_state: { shown: boolean, showButton: boolean },
notify_channel_info: { treeEntryId: number, info: ChannelEntryInfo },
notify_channel_icon: { treeEntryId: number, icon: ClientIcon },

View File

@ -6,7 +6,7 @@ import {
ClientIcons,
ClientNameInfo, ClientTalkIconState, ServerState
} 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 {ChannelIconClass, ChannelIconsRenderer, RendererChannel} from "tc-shared/ui/tree/RendererChannel";
import {ClientIcon} from "svg-sprites/client-icons";
@ -69,6 +69,10 @@ export class RDPChannelTree {
readonly refMove = React.createRef<RendererMove>();
readonly refTree = React.createRef<ChannelTreeView>();
readonly refPopoutButton = React.createRef<PopoutButton>();
popoutShown: boolean = false;
popoutButtonShown: boolean = false;
private treeRevision: number = 0;
private orderedTree: RDPEntry[] = [];
@ -198,6 +202,7 @@ export class RDPChannelTree {
}));
this.events.fire("query_tree_entries");
this.events.fire("query_popout_state");
}
destroy() {
@ -268,6 +273,13 @@ export class RDPChannelTree {
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 {

View File

@ -14,15 +14,21 @@ import {ClientIcon} from "svg-sprites/client-icons";
const viewStyle = require("./View.scss");
const PopoutButton = (props: {}) => {
return (
<div className={viewStyle.popoutButton}>
<div className={viewStyle.button}>
<ClientIconRenderer icon={ClientIcon.ChannelPopout} />
export class PopoutButton extends React.Component<{ tree: RDPChannelTree }, {}> {
render() {
if(!this.props.tree.popoutButtonShown) {
return null;
}
return (
<div className={viewStyle.popoutButton} onClick={() => this.props.tree.events.fire("action_toggle_popout", { shown: !this.props.tree.popoutShown })}>
<div className={viewStyle.button} title={this.props.tree.popoutShown ? tr("Popin the second channel tree view") : tr("Popout the channel tree view")}>
<ClientIconRenderer icon={this.props.tree.popoutShown ? ClientIcon.ChannelPopin : ClientIcon.ChannelPopout} />
</div>
</div>
</div>
)
};
);
}
}
export interface ChannelTreeViewProperties {
events: Registry<ChannelTreeUIEvents>;
@ -203,7 +209,7 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
ref={this.props.dataProvider.refMove}
/>
</div>
<PopoutButton />
<PopoutButton tree={this.props.dataProvider} ref={this.props.dataProvider.refPopoutButton} />
</div>
)
}

View File

@ -1,4 +1,3 @@
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {Registry} from "tc-shared/events";
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
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 {
initializePopoutControlBarController
} 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) {
const eventsTree = new Registry<ChannelTreeUIEvents>();
eventsTree.enableDebug("channel-tree-view-modal");
initializeChannelTreeController(eventsTree, handler.channelTree);
export class ChannelTreePopoutController {
readonly channelTree: ChannelTree;
const eventsControlBar = new Registry<ControlBarEvents>();
initializePopoutControlBarController(eventsControlBar, handler);
private popoutInstance: ModalController;
private uiEvents: Registry<ChannelTreePopoutEvents>;
private treeEvents: Registry<ChannelTreeUIEvents>;
private controlBarEvents: Registry<ControlBarEvents>;
let handlerDestroyListener;
server_connections.events().on("notify_handler_deleted", handlerDestroyListener = event => {
if(event.handler !== handler) {
private generalEvents: (() => void)[];
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;
}
modal.destroy();
});
this.uiEvents = new Registry<ChannelTreePopoutEvents>();
this.uiEvents.on("query_title", () => this.sendTitle());
const modal = spawnExternalModal("channel-tree", { tree: eventsTree, controlBar: eventsControlBar }, { handlerId: handler.handlerId }, "channel-tree-" + handler.handlerId);
modal.show();
this.treeEvents = new Registry<ChannelTreeUIEvents>();
initializeChannelTreeController(this.treeEvents, this.channelTree, { popoutButton: false });
modal.getEvents().on("destroy", () => {
server_connections.events().off("notify_handler_deleted", handlerDestroyListener);
this.controlBarEvents = new Registry<ControlBarEvents>();
initializePopoutControlBarController(this.controlBarEvents, this.channelTree.client);
eventsTree.fire("notify_destroy");
eventsTree.destroy();
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);
eventsControlBar.fire("notify_destroy");
eventsControlBar.destroy();
});
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();
this.channelTree.events.fire("notify_popout_state_changed", { popoutShown: true });
}
popin() {
if(!this.popoutInstance) { return; }
this.popoutInstance.destroy();
this.popoutInstance = undefined; /* not needed, but just to ensure (will be set within the destroy callback already) */
}
private sendTitle() {
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 });
}
}

View File

@ -0,0 +1,4 @@
export interface ChannelTreePopoutEvents {
query_title: {},
notify_title: { title: string }
}

View File

@ -2,13 +2,25 @@ import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions";
import {Registry, RegistryMap} from "tc-shared/events";
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
import * as React from "react";
import {Translatable} from "tc-shared/ui/react-elements/i18n";
import {ChannelTreeRenderer} from "tc-shared/ui/tree/Renderer";
import {ControlBarEvents} from "tc-shared/ui/frames/control-bar/Definitions";
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");
class ChannelTreeModal extends AbstractModal {
readonly eventsUI: Registry<ChannelTreePopoutEvents>;
readonly eventsTree: Registry<ChannelTreeUIEvents>;
readonly eventsControlBar: Registry<ControlBarEvents>;
@ -18,8 +30,15 @@ class ChannelTreeModal extends AbstractModal {
super();
this.handlerId = userData.handlerId;
this.eventsUI = registryMap["base"] as any;
this.eventsTree = registryMap["tree"] as any;
this.eventsControlBar = registryMap["controlBar"] as any;
this.eventsUI.fire("query_title");
}
protected onDestroy() {
super.onDestroy();
}
renderBody(): React.ReactElement {
@ -35,8 +54,8 @@ class ChannelTreeModal extends AbstractModal {
)
}
title(): string | React.ReactElement<Translatable> {
return <Translatable>Channel tree</Translatable>;
title(): React.ReactElement {
return <TitleRenderer events={this.eventsUI} />;
}
}

View File

@ -542,7 +542,7 @@ export class VoiceConnection extends AbstractVoiceConnection {
logWarn(LogCategory.CLIENT, tr("Failed to clear the whisper target: %o"), error);
});
}
this.voiceBridge.stopWhispering();
this.voiceBridge?.stopWhispering();
}
}