Implementing channel tree client move semantics
parent
8e2d35c683
commit
f81f3d6d3d
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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: {}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue