From f81f3d6d3d802c5ffbe1db16db301bebd6fd38f0 Mon Sep 17 00:00:00 2001 From: WolverinDEV Date: Sun, 27 Sep 2020 16:49:04 +0200 Subject: [PATCH] Implementing channel tree client move semantics --- shared/js/connection/CommandHandler.ts | 1 + shared/js/tree/ChannelTree.tsx | 4 + shared/js/ui/tree/Controller.tsx | 42 +++++++- shared/js/ui/tree/Definitions.ts | 3 + shared/js/ui/tree/RendererDataProvider.tsx | 17 ++- shared/js/ui/tree/RendererMove.tsx | 116 +++++++++++++++++++++ shared/js/ui/tree/RendererView.tsx | 16 ++- shared/js/ui/tree/TreeEntryMove.tsx | 99 ------------------ 8 files changed, 194 insertions(+), 104 deletions(-) create mode 100644 shared/js/ui/tree/RendererMove.tsx delete mode 100644 shared/js/ui/tree/TreeEntryMove.tsx diff --git a/shared/js/connection/CommandHandler.ts b/shared/js/connection/CommandHandler.ts index 021ca48e..3c00520c 100644 --- a/shared/js/connection/CommandHandler.ts +++ b/shared/js/connection/CommandHandler.ts @@ -356,6 +356,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler { handleCommandChannelListFinished() { + this.connection.client.channelTree.channelsInitialized = true; this.connection.client.channelTree.events.fire_async("notify_channel_list_received"); if(this.batch_update_finished_timeout) { diff --git a/shared/js/tree/ChannelTree.tsx b/shared/js/tree/ChannelTree.tsx index 0447e3ce..3ef85f6e 100644 --- a/shared/js/tree/ChannelTree.tsx +++ b/shared/js/tree/ChannelTree.tsx @@ -228,6 +228,9 @@ export class ChannelTree { channels: ChannelEntry[] = []; clients: ClientEntry[] = []; + /* whatever all channels have been initiaized */ + channelsInitialized: boolean = false; + //readonly view: React.RefObject; //readonly view_move: React.RefObject; readonly selection: ChannelTreeEntrySelect; @@ -875,6 +878,7 @@ export class ChannelTree { } reset() { + this.channelsInitialized = false; batch_updates(BatchUpdateType.CHANNEL_TREE); this.selection.clear_selection(); diff --git a/shared/js/ui/tree/Controller.tsx b/shared/js/ui/tree/Controller.tsx index ff74f45e..5b1e1222 100644 --- a/shared/js/ui/tree/Controller.tsx +++ b/shared/js/ui/tree/Controller.tsx @@ -108,8 +108,10 @@ class ChannelTreeController { this.initializeServerEvents(this.channelTree.server); this.channelTree.events.register_handler(this); - this.channelTree.channels.forEach(channel => this.initializeChannelEvents(channel)); - this.channelTree.clients.forEach(client => this.initializeClientEvents(client)); + + if(this.channelTree.channelsInitialized) { + this.handleChannelListReceived(); + } } destroy() { @@ -688,6 +690,11 @@ function initializeTreeController(events: Registry, channel return; } + if(moveSelection) { + /* don't select entries while we're moving */ + return; + } + channelTree.events.fire("action_select_entries", { entries: [entry], mode: "exclusive" @@ -732,6 +739,37 @@ function initializeTreeController(events: Registry, channel }) }); + let moveSelection: ClientEntry[]; + events.on("action_start_entry_move", event => { + const selection = channelTree.selection.selected_entries.slice(); + if(selection.length === 0) { return; } + if(selection.findIndex(element => !(element instanceof ClientEntry)) !== -1) { return; } + + moveSelection = selection as any; + events.fire_async("notify_entry_move", { entries: selection.map(client => (client as ClientEntry).clientNickName()).join(", "), begin: event.start, current: event.current }); + }); + + events.on("action_move_entries", event => { + if(event.treeEntryId === 0 || !moveSelection?.length) { + moveSelection = undefined; + return; + } + + const entry = channelTree.findEntryId(event.treeEntryId); + if(!entry || !(entry instanceof ChannelEntry)) { + logWarn(LogCategory.CHANNEL, tr("Tried to move clients to an invalid tree entry with id %o"), event.treeEntryId); + return; + } + + moveSelection.filter(e => e.currentChannel() !== entry).forEach(e => { + channelTree.client.serverConnection.send_command("clientmove", { + clid: e.clientId(), + cid: entry.channelId + }); + }); + moveSelection = undefined; + }); + events.on("notify_client_name_edit_failed", event => { const entry = channelTree.findEntryId(event.treeEntryId); if(!entry || !(entry instanceof LocalClientEntry)) { diff --git a/shared/js/ui/tree/Definitions.ts b/shared/js/ui/tree/Definitions.ts index 76ab09c5..8eab9fb0 100644 --- a/shared/js/ui/tree/Definitions.ts +++ b/shared/js/ui/tree/Definitions.ts @@ -40,6 +40,7 @@ export interface ChannelTreeUIEvents { action_channel_open_file_browser: { treeEntryId: number }, action_client_double_click: { treeEntryId: number }, action_client_name_submit: { treeEntryId: number, name: string }, + action_move_entries: { treeEntryId: number /* zero if move failed */ } /* queries */ query_tree_entries: {}, @@ -76,6 +77,8 @@ export interface ChannelTreeUIEvents { notify_unread_state: { treeEntryId: number, unread: boolean }, notify_select_state: { treeEntryId: number, selected: boolean }, + notify_entry_move: { entries: string, begin: { x: number, y: number }, current: { x: number, y: number } }, + notify_visibility_changed: { visible: boolean }, notify_destroy: {} } \ No newline at end of file diff --git a/shared/js/ui/tree/RendererDataProvider.tsx b/shared/js/ui/tree/RendererDataProvider.tsx index 9a0b0934..cf918761 100644 --- a/shared/js/ui/tree/RendererDataProvider.tsx +++ b/shared/js/ui/tree/RendererDataProvider.tsx @@ -20,6 +20,7 @@ import { RendererClient } from "tc-shared/ui/tree/RendererClient"; import {ServerRenderer} from "tc-shared/ui/tree/RendererServer"; +import {RendererMove} from "./RendererMove"; function isEquivalent(a, b) { const typeA = typeof a; @@ -66,8 +67,10 @@ export class RDPChannelTree { private registeredEventHandlers = []; + readonly refMove = React.createRef(); readonly refTree = React.createRef(); + private treeRevision: number = 0; private orderedTree: RDPEntry[] = []; private treeEntries: {[key: number]: RDPEntry} = {}; @@ -250,9 +253,21 @@ export class RDPChannelTree { }); this.refTree?.current.setState({ - tree: this.orderedTree.slice() + tree: this.orderedTree.slice(), + treeRevision: ++this.treeRevision }); } + + + @EventHandler("notify_entry_move") + private handleNotifyEntryMove(event: ChannelTreeUIEvents["notify_entry_move"]) { + if(!this.refMove.current) { + this.events.fire_async("action_move_entries", { treeEntryId: 0 }); + return; + } + + this.refMove.current.enableEntryMove(event.entries, event.begin, event.current); + } } export abstract class RDPEntry { diff --git a/shared/js/ui/tree/RendererMove.tsx b/shared/js/ui/tree/RendererMove.tsx new file mode 100644 index 00000000..8a1b7ac1 --- /dev/null +++ b/shared/js/ui/tree/RendererMove.tsx @@ -0,0 +1,116 @@ +import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase"; +import * as React from "react"; +import * as ReactDOM from "react-dom"; +import {LogCategory, logWarn} from "tc-shared/log"; + +const moveStyle = require("./TreeEntryMove.scss"); + +export interface TreeEntryMoveProps { + onMoveEnd: (point: { x: number, y: number }) => void; + onMoveCancel: () => void; +} + +export interface TreeEntryMoveState { + active: boolean; + begin: { x: number, y: number }; + description: string; +} + +export class RendererMove extends ReactComponentBase { + private readonly domContainer; + private readonly documentMouseOutListener; + private readonly documentMouseListener; + private readonly refContainer: React.RefObject; + + private current: { x: number, y: number }; + + constructor(props) { + super(props); + + this.refContainer = React.createRef(); + + this.domContainer = document.createElement("div"); + this.documentMouseOutListener = (e: MouseEvent) => { + if (e.type === "mouseup") { + if (e.button !== 0) return; + + this.props.onMoveEnd({x: e.pageX, y: e.pageY}); + } else { + this.props.onMoveCancel(); + } + + this.disableEntryMove(); + }; + + this.documentMouseListener = (e: MouseEvent) => { + this.current = {x: e.pageX, y: e.pageY}; + const container = this.refContainer.current; + if (!container) return; + + container.style.top = e.pageY + "px"; + container.style.left = e.pageX + "px"; + }; + } + + componentDidMount() { + document.body.append(this.domContainer); + } + + componentWillUnmount() { + this.domContainer.remove(); + } + + enableEntryMove(description: string, begin: { x: number, y: number }, current: { x: number, y: number }, callback_enabled?: () => void) { + this.current = current; + this.setState({ + active: true, + begin: begin, + description: description + }, callback_enabled); + + document.addEventListener("mousemove", this.documentMouseListener); + document.addEventListener("mouseleave", this.documentMouseOutListener); + document.addEventListener("mouseup", this.documentMouseOutListener); + } + + private disableEntryMove() { + this.setState({ + active: false + }); + document.removeEventListener("mousemove", this.documentMouseListener); + document.removeEventListener("mouseleave", this.documentMouseOutListener); + document.removeEventListener("mouseup", this.documentMouseOutListener); + } + + protected defaultState(): TreeEntryMoveState { + return { + active: false, + begin: { x: 0, y: 0 }, + description: "" + } + } + + isActive() { + return this.state.active; + } + + render() { + if (!this.state.active) + return null; + + return ReactDOM.createPortal(this.renderPortal(), this.domContainer); + } + + private renderPortal() { + if(!this.current) { + logWarn(LogCategory.CHANNEL, tr("Tried to render the move container without a current position")); + return null; + } + return ( +
+ {this.state.description} +
+ ); + } +} \ No newline at end of file diff --git a/shared/js/ui/tree/RendererView.tsx b/shared/js/ui/tree/RendererView.tsx index 15af45e9..bf335609 100644 --- a/shared/js/ui/tree/RendererView.tsx +++ b/shared/js/ui/tree/RendererView.tsx @@ -8,6 +8,7 @@ import * as React from "react"; import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions"; import ResizeObserver from 'resize-observer-polyfill'; import {RDPEntry, RDPChannelTree} from "./RendererDataProvider"; +import {RendererMove} from "./RendererMove"; const viewStyle = require("./View.scss"); @@ -27,6 +28,7 @@ export interface ChannelTreeViewState { /* the currently rendered tree */ tree: RDPEntry[]; + treeRevision: number; } @ReactEventHandler(e => e.props.events) @@ -61,7 +63,8 @@ export class ChannelTreeView extends ReactComponentBase { @@ -164,7 +167,6 @@ export class ChannelTreeView extends ReactComponentBase {elements} + { + const targetEntry = this.getEntryFromPoint(target.x, target.y); + this.props.events.fire("action_move_entries", { treeEntryId: typeof targetEntry === "number" ? targetEntry : 0 }); + }} + onMoveCancel={() => { + this.props.events.fire("action_move_entries", { treeEntryId: 0 }); + }} + ref={this.props.dataProvider.refMove} + /> ) } diff --git a/shared/js/ui/tree/TreeEntryMove.tsx b/shared/js/ui/tree/TreeEntryMove.tsx deleted file mode 100644 index ac2de7fa..00000000 --- a/shared/js/ui/tree/TreeEntryMove.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase"; -import * as React from "react"; -import * as ReactDOM from "react-dom"; -import {ChannelTreeView} from "tc-shared/ui/tree/RendererView"; - -const moveStyle = require("./TreeEntryMove.scss"); - -export interface TreeEntryMoveProps { - onMoveEnd: (point: { x: number, y: number }) => void; -} - -export interface TreeEntryMoveState { - tree_view: ChannelTreeView; - - begin: { x: number, y: number }; - description: string; -} - -export class TreeEntryMove extends ReactComponentBase { - private readonly domContainer; - private readonly document_mouse_out_listener; - private readonly document_mouse_listener; - private readonly ref_container: React.RefObject; - - private current: { x: number, y: number }; - - constructor(props) { - super(props); - - this.ref_container = React.createRef(); - this.domContainer = document.getElementById("mouse-move"); - this.document_mouse_out_listener = (e: MouseEvent) => { - if (e.type === "mouseup") { - if (e.button !== 0) return; - - this.props.onMoveEnd({x: e.pageX, y: e.pageY}); - } - - this.disableEntryMove(); - }; - - this.document_mouse_listener = (e: MouseEvent) => { - this.current = {x: e.pageX, y: e.pageY}; - const container = this.ref_container.current; - if (!container) return; - - container.style.top = e.pageY + "px"; - container.style.left = e.pageX + "px"; - }; - } - - enableEntryMove(view: ChannelTreeView, description: string, begin: { x: number, y: null }, current: { x: number, y: null }, callback_enabled?: () => void) { - this.setState({ - tree_view: view, - begin: begin, - description: description - }, callback_enabled); - - this.current = current; - document.addEventListener("mousemove", this.document_mouse_listener); - document.addEventListener("mouseleave", this.document_mouse_out_listener); - document.addEventListener("mouseup", this.document_mouse_out_listener); - } - - private disableEntryMove() { - this.setState({ - tree_view: null - }); - document.removeEventListener("mousemove", this.document_mouse_listener); - document.removeEventListener("mouseleave", this.document_mouse_out_listener); - document.removeEventListener("mouseup", this.document_mouse_out_listener); - } - - protected defaultState(): TreeEntryMoveState { - return { - tree_view: null, - begin: {x: 0, y: 0}, - description: "" - } - } - - isActive() { - return !!this.state.tree_view; - } - - render() { - if (!this.state.tree_view) - return null; - - return ReactDOM.createPortal(this.renderPortal(), this.domContainer); - } - - private renderPortal() { - return
- {this.state.description} -
; - } -} \ No newline at end of file