Implementing channel tree client move semantics
This commit is contained in:
parent
940a51689e
commit
206b814fc4
8 changed files with 194 additions and 104 deletions
|
@ -356,6 +356,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
||||||
|
|
||||||
|
|
||||||
handleCommandChannelListFinished() {
|
handleCommandChannelListFinished() {
|
||||||
|
this.connection.client.channelTree.channelsInitialized = true;
|
||||||
this.connection.client.channelTree.events.fire_async("notify_channel_list_received");
|
this.connection.client.channelTree.events.fire_async("notify_channel_list_received");
|
||||||
|
|
||||||
if(this.batch_update_finished_timeout) {
|
if(this.batch_update_finished_timeout) {
|
||||||
|
|
|
@ -228,6 +228,9 @@ export class ChannelTree {
|
||||||
channels: ChannelEntry[] = [];
|
channels: ChannelEntry[] = [];
|
||||||
clients: ClientEntry[] = [];
|
clients: ClientEntry[] = [];
|
||||||
|
|
||||||
|
/* whatever all channels have been initiaized */
|
||||||
|
channelsInitialized: boolean = false;
|
||||||
|
|
||||||
//readonly view: React.RefObject<ChannelTreeView>;
|
//readonly view: React.RefObject<ChannelTreeView>;
|
||||||
//readonly view_move: React.RefObject<TreeEntryMove>;
|
//readonly view_move: React.RefObject<TreeEntryMove>;
|
||||||
readonly selection: ChannelTreeEntrySelect;
|
readonly selection: ChannelTreeEntrySelect;
|
||||||
|
@ -875,6 +878,7 @@ export class ChannelTree {
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
|
this.channelsInitialized = false;
|
||||||
batch_updates(BatchUpdateType.CHANNEL_TREE);
|
batch_updates(BatchUpdateType.CHANNEL_TREE);
|
||||||
|
|
||||||
this.selection.clear_selection();
|
this.selection.clear_selection();
|
||||||
|
|
|
@ -108,8 +108,10 @@ class ChannelTreeController {
|
||||||
this.initializeServerEvents(this.channelTree.server);
|
this.initializeServerEvents(this.channelTree.server);
|
||||||
|
|
||||||
this.channelTree.events.register_handler(this);
|
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() {
|
destroy() {
|
||||||
|
@ -688,6 +690,11 @@ function initializeTreeController(events: Registry<ChannelTreeUIEvents>, channel
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(moveSelection) {
|
||||||
|
/* don't select entries while we're moving */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
channelTree.events.fire("action_select_entries", {
|
channelTree.events.fire("action_select_entries", {
|
||||||
entries: [entry],
|
entries: [entry],
|
||||||
mode: "exclusive"
|
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 => {
|
events.on("notify_client_name_edit_failed", event => {
|
||||||
const entry = channelTree.findEntryId(event.treeEntryId);
|
const entry = channelTree.findEntryId(event.treeEntryId);
|
||||||
if(!entry || !(entry instanceof LocalClientEntry)) {
|
if(!entry || !(entry instanceof LocalClientEntry)) {
|
||||||
|
|
|
@ -40,6 +40,7 @@ export interface ChannelTreeUIEvents {
|
||||||
action_channel_open_file_browser: { treeEntryId: number },
|
action_channel_open_file_browser: { treeEntryId: number },
|
||||||
action_client_double_click: { treeEntryId: number },
|
action_client_double_click: { treeEntryId: number },
|
||||||
action_client_name_submit: { treeEntryId: number, name: string },
|
action_client_name_submit: { treeEntryId: number, name: string },
|
||||||
|
action_move_entries: { treeEntryId: number /* zero if move failed */ }
|
||||||
|
|
||||||
/* queries */
|
/* queries */
|
||||||
query_tree_entries: {},
|
query_tree_entries: {},
|
||||||
|
@ -76,6 +77,8 @@ export interface ChannelTreeUIEvents {
|
||||||
notify_unread_state: { treeEntryId: number, unread: boolean },
|
notify_unread_state: { treeEntryId: number, unread: boolean },
|
||||||
notify_select_state: { treeEntryId: number, selected: 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_visibility_changed: { visible: boolean },
|
||||||
notify_destroy: {}
|
notify_destroy: {}
|
||||||
}
|
}
|
|
@ -20,6 +20,7 @@ import {
|
||||||
RendererClient
|
RendererClient
|
||||||
} from "tc-shared/ui/tree/RendererClient";
|
} from "tc-shared/ui/tree/RendererClient";
|
||||||
import {ServerRenderer} from "tc-shared/ui/tree/RendererServer";
|
import {ServerRenderer} from "tc-shared/ui/tree/RendererServer";
|
||||||
|
import {RendererMove} from "./RendererMove";
|
||||||
|
|
||||||
function isEquivalent(a, b) {
|
function isEquivalent(a, b) {
|
||||||
const typeA = typeof a;
|
const typeA = typeof a;
|
||||||
|
@ -66,8 +67,10 @@ export class RDPChannelTree {
|
||||||
|
|
||||||
private registeredEventHandlers = [];
|
private registeredEventHandlers = [];
|
||||||
|
|
||||||
|
readonly refMove = React.createRef<RendererMove>();
|
||||||
readonly refTree = React.createRef<ChannelTreeView>();
|
readonly refTree = React.createRef<ChannelTreeView>();
|
||||||
|
|
||||||
|
private treeRevision: number = 0;
|
||||||
private orderedTree: RDPEntry[] = [];
|
private orderedTree: RDPEntry[] = [];
|
||||||
private treeEntries: {[key: number]: RDPEntry} = {};
|
private treeEntries: {[key: number]: RDPEntry} = {};
|
||||||
|
|
||||||
|
@ -250,9 +253,21 @@ export class RDPChannelTree {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.refTree?.current.setState({
|
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 {
|
export abstract class RDPEntry {
|
||||||
|
|
116
shared/js/ui/tree/RendererMove.tsx
Normal file
116
shared/js/ui/tree/RendererMove.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import * as React from "react";
|
||||||
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
|
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
|
||||||
import ResizeObserver from 'resize-observer-polyfill';
|
import ResizeObserver from 'resize-observer-polyfill';
|
||||||
import {RDPEntry, RDPChannelTree} from "./RendererDataProvider";
|
import {RDPEntry, RDPChannelTree} from "./RendererDataProvider";
|
||||||
|
import {RendererMove} from "./RendererMove";
|
||||||
|
|
||||||
const viewStyle = require("./View.scss");
|
const viewStyle = require("./View.scss");
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@ export interface ChannelTreeViewState {
|
||||||
|
|
||||||
/* the currently rendered tree */
|
/* the currently rendered tree */
|
||||||
tree: RDPEntry[];
|
tree: RDPEntry[];
|
||||||
|
treeRevision: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactEventHandler<ChannelTreeView>(e => e.props.events)
|
@ReactEventHandler<ChannelTreeView>(e => e.props.events)
|
||||||
|
@ -61,7 +63,8 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
|
||||||
view_height: 0,
|
view_height: 0,
|
||||||
tree_version: 0,
|
tree_version: 0,
|
||||||
smoothScroll: false,
|
smoothScroll: false,
|
||||||
tree: []
|
tree: [],
|
||||||
|
treeRevision: -1
|
||||||
};
|
};
|
||||||
|
|
||||||
this.documentMouseListener = (e: MouseEvent) => {
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={viewStyle.channelTreeContainer + " " + (this.state.smoothScroll ? viewStyle.smoothScroll : "")}
|
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"}}>
|
style={{height: (this.state.tree.length * ChannelTreeView.EntryHeight) + "px"}}>
|
||||||
{elements}
|
{elements}
|
||||||
</div>
|
</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>
|
</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…
Add table
Reference in a new issue