Implementing channel tree client move semantics

canary
WolverinDEV 2020-09-27 16:49:04 +02:00
parent 8e2d35c683
commit f81f3d6d3d
8 changed files with 194 additions and 104 deletions

View File

@ -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) {

View File

@ -228,6 +228,9 @@ export class ChannelTree {
channels: ChannelEntry[] = [];
clients: ClientEntry[] = [];
/* whatever all channels have been initiaized */
channelsInitialized: boolean = false;
//readonly view: React.RefObject<ChannelTreeView>;
//readonly view_move: React.RefObject<TreeEntryMove>;
readonly selection: ChannelTreeEntrySelect;
@ -875,6 +878,7 @@ export class ChannelTree {
}
reset() {
this.channelsInitialized = false;
batch_updates(BatchUpdateType.CHANNEL_TREE);
this.selection.clear_selection();

View File

@ -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<ChannelTreeUIEvents>, 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<ChannelTreeUIEvents>, 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)) {

View File

@ -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: {}
}

View File

@ -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<RendererMove>();
readonly refTree = React.createRef<ChannelTreeView>();
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<ChannelTreeUIEvents>("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 {

View File

@ -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<TreeEntryMoveProps, TreeEntryMoveState> {
private readonly domContainer;
private readonly documentMouseOutListener;
private readonly documentMouseListener;
private readonly refContainer: React.RefObject<HTMLDivElement>;
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 (
<div style={{ top: this.current.y, left: this.current.x }} className={moveStyle.moveContainer}
ref={this.refContainer}>
{this.state.description}
</div>
);
}
}

View File

@ -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<ChannelTreeView>(e => e.props.events)
@ -61,7 +63,8 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
view_height: 0,
tree_version: 0,
smoothScroll: false,
tree: []
tree: [],
treeRevision: -1
};
this.documentMouseListener = (e: MouseEvent) => {
@ -164,7 +167,6 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
}
}
/* className={this.classList(viewStyle.channelTree, this.props.tree.isClientMoveActive() && viewStyle.move)} */
return (
<div
className={viewStyle.channelTreeContainer + " " + (this.state.smoothScroll ? viewStyle.smoothScroll : "")}
@ -177,6 +179,16 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
style={{height: (this.state.tree.length * ChannelTreeView.EntryHeight) + "px"}}>
{elements}
</div>
<RendererMove
onMoveEnd={target => {
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}
/>
</div>
)
}

View File

@ -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<TreeEntryMoveProps, TreeEntryMoveState> {
private readonly domContainer;
private readonly document_mouse_out_listener;
private readonly document_mouse_listener;
private readonly ref_container: React.RefObject<HTMLDivElement>;
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 <div style={{top: this.current.y, left: this.current.x}} className={moveStyle.moveContainer}
ref={this.ref_container}>
{this.state.description}
</div>;
}
}