Implementing key tree control for the external channel trees

This commit is contained in:
WolverinDEV 2020-09-29 15:42:54 +02:00 committed by WolverinDEV
parent 9ab430e8db
commit 883ccbbc65
6 changed files with 285 additions and 329 deletions

View file

@ -4,7 +4,7 @@ import * as log from "tc-shared/log";
import {LogCategory, logWarn} from "tc-shared/log"; import {LogCategory, logWarn} from "tc-shared/log";
import {Settings, settings} from "tc-shared/settings"; import {Settings, settings} from "tc-shared/settings";
import {PermissionType} from "tc-shared/permission/PermissionType"; import {PermissionType} from "tc-shared/permission/PermissionType";
import {KeyCode, SpecialKey} from "tc-shared/PPTListener"; import {SpecialKey} from "tc-shared/PPTListener";
import {Sound} from "tc-shared/sound/Sounds"; import {Sound} from "tc-shared/sound/Sounds";
import {Group} from "tc-shared/permission/GroupManager"; import {Group} from "tc-shared/permission/GroupManager";
import {ServerAddress, ServerEntry} from "./Server"; import {ServerAddress, ServerEntry} from "./Server";
@ -86,14 +86,14 @@ export interface ChannelTreeEvents {
export class ChannelTreeEntrySelect { export class ChannelTreeEntrySelect {
readonly handle: ChannelTree; readonly handle: ChannelTree;
selected_entries: ChannelTreeEntry<any>[] = []; selectedEntries: ChannelTreeEntry<any>[] = [];
private readonly handler_select_entries; private readonly handlerSelectEntries;
constructor(handle: ChannelTree) { constructor(handle: ChannelTree) {
this.handle = handle; this.handle = handle;
this.handler_select_entries = e => { this.handlerSelectEntries = e => {
batch_updates(BatchUpdateType.CHANNEL_TREE); batch_updates(BatchUpdateType.CHANNEL_TREE);
try { try {
this.handleSelectEntries(e) this.handleSelectEntries(e)
@ -102,122 +102,248 @@ export class ChannelTreeEntrySelect {
} }
}; };
this.handle.events.on("action_select_entries", this.handler_select_entries); this.handle.events.on("action_select_entries", this.handlerSelectEntries);
} }
reset() { reset() {
this.selected_entries.splice(0, this.selected_entries.length); this.selectedEntries.splice(0, this.selectedEntries.length);
} }
destroy() { destroy() {
this.handle.events.off("action_select_entries", this.handler_select_entries); this.handle.events.off("action_select_entries", this.handlerSelectEntries);
this.selected_entries.splice(0, this.selected_entries.length); this.selectedEntries.splice(0, this.selectedEntries.length);
} }
is_multi_select() { isMultiSelect() {
return this.selected_entries.length > 1; return this.selectedEntries.length > 1;
} }
is_anything_selected() { isAnythingSelected() {
return this.selected_entries.length > 0; return this.selectedEntries.length > 0;
} }
clear_selection() { clearSelection() {
this.handleSelectEntries({ this.handleSelectEntries({
entries: [], entries: [],
mode: "exclusive" mode: "exclusive"
}); });
} }
private handleSelectEntries(event: ChannelTreeEvents["action_select_entries"]) { /**
if(event.mode === "exclusive") { * auto := Select/unselect/add/remove depending on the selected state & shift key state
let deleted_entries = this.selected_entries; * exclusive := Only selected these entries
* append := Append these entries to the current selection
* remove := Remove these entries from the current selection
*/
select(entries: ChannelTreeEntry<any>[], mode: "auto" | "exclusive" | "append" | "remove") {
entries = entries.filter(entry => !!entry);
if(mode === "exclusive") {
let deleted_entries = this.selectedEntries;
let new_entries = []; let new_entries = [];
this.selected_entries = []; this.selectedEntries = [];
for(const new_entry of event.entries) { for(const new_entry of entries) {
if(!deleted_entries.remove(new_entry)) if(!deleted_entries.remove(new_entry)) {
new_entries.push(new_entry); new_entries.push(new_entry);
this.selected_entries.push(new_entry); }
this.selectedEntries.push(new_entry);
} }
for(const deleted of deleted_entries) for(const deleted of deleted_entries) {
deleted["onUnselect"](); deleted["onUnselect"]();
}
for(const new_entry of new_entries) for(const new_entry of new_entries) {
new_entry["onSelect"](!this.is_multi_select()); new_entry["onSelect"](!this.isMultiSelect());
}
if(deleted_entries.length !== 0 || new_entries.length !== 0) if(deleted_entries.length !== 0 || new_entries.length !== 0) {
this.handle.events.fire("notify_selection_changed"); this.handle.events.fire("notify_selection_changed");
} else if(event.mode === "append") { }
} else if(mode === "append") {
let new_entries = []; let new_entries = [];
for(const entry of event.entries) { for(const entry of entries) {
if(this.selected_entries.findIndex(e => e === entry) !== -1) if(this.selectedEntries.findIndex(e => e === entry) !== -1)
continue; continue;
this.selected_entries.push(entry); this.selectedEntries.push(entry);
new_entries.push(entry); new_entries.push(entry);
} }
for(const new_entry of new_entries) for(const new_entry of new_entries) {
new_entry["onSelect"](!this.is_multi_select()); new_entry["onSelect"](!this.isMultiSelect());
if(new_entries.length !== 0)
this.handle.events.fire("notify_selection_changed");
} else if(event.mode === "remove") {
let deleted_entries = [];
for(const entry of event.entries) {
if(this.selected_entries.remove(entry))
deleted_entries.push(entry);
} }
for(const deleted of deleted_entries) if(new_entries.length !== 0) {
deleted["onUnselect"]();
if(deleted_entries.length !== 0)
this.handle.events.fire("notify_selection_changed"); this.handle.events.fire("notify_selection_changed");
} else if(event.mode === "auto") { }
} else if(mode === "remove") {
let deleted_entries = [];
for(const entry of entries) {
if(this.selectedEntries.remove(entry)) {
deleted_entries.push(entry);
}
}
for(const deleted of deleted_entries) {
deleted["onUnselect"]();
}
if(deleted_entries.length !== 0) {
this.handle.events.fire("notify_selection_changed");
}
} else if(mode === "auto") {
let deleted_entries = []; let deleted_entries = [];
let new_entries = []; let new_entries = [];
if(ppt.key_pressed(SpecialKey.SHIFT)) { if(ppt.key_pressed(SpecialKey.SHIFT)) {
for(const entry of event.entries) { for(const entry of entries) {
const index = this.selected_entries.findIndex(e => e === entry); const index = this.selectedEntries.findIndex(e => e === entry);
if(index === -1) { if(index === -1) {
this.selected_entries.push(entry); this.selectedEntries.push(entry);
new_entries.push(entry); new_entries.push(entry);
} else { } else {
this.selected_entries.splice(index, 1); this.selectedEntries.splice(index, 1);
deleted_entries.push(entry); deleted_entries.push(entry);
} }
} }
} else { } else {
deleted_entries = this.selected_entries.splice(0, this.selected_entries.length); deleted_entries = this.selectedEntries.splice(0, this.selectedEntries.length);
if(event.entries.length !== 0) { if(entries.length !== 0) {
const entry = event.entries[event.entries.length - 1]; const entry = entries[entries.length - 1];
this.selected_entries.push(entry); this.selectedEntries.push(entry);
if(!deleted_entries.remove(entry)) if(!deleted_entries.remove(entry))
new_entries.push(entry); /* entry wans't selected yet */ new_entries.push(entry); /* entry wans't selected yet */
} }
} }
for(const deleted of deleted_entries) for(const deleted of deleted_entries) {
deleted["onUnselect"](); deleted["onUnselect"]();
}
for(const new_entry of new_entries) for(const new_entry of new_entries) {
new_entry["onSelect"](!this.is_multi_select()); new_entry["onSelect"](!this.isMultiSelect());
}
if(deleted_entries.length !== 0 || new_entries.length !== 0) if(deleted_entries.length !== 0 || new_entries.length !== 0) {
this.handle.events.fire("notify_selection_changed"); this.handle.events.fire("notify_selection_changed");
}
} else { } else {
console.warn("Received entry select event with unknown mode: %s", event.mode); console.warn("Received entry select event with unknown mode: %s", mode);
} }
/* /*
TODO! TODO!
if(this.selected_entries.length === 1) if(this.selected_entries.length === 1)
this.handle.view.current?.scrollEntryInView(this.selected_entries[0] as any); this.handle.view.current?.scrollEntryInView(this.selected_entries[0] as any);
*/ */
}
private selectNextChannel(currentChannel: ChannelEntry, selectClients: boolean) {
if(selectClients) {
const clients = currentChannel.channelClientsOrdered();
if(clients.length > 0) {
this.select([clients[0]], "exclusive");
return;
}
}
const children = currentChannel.children();
if(children.length > 0) {
this.select([children[0]], "exclusive")
return;
}
const next = currentChannel.channel_next;
if(next) {
this.select([next], "exclusive")
return;
}
let parent = currentChannel.parent_channel();
while(parent) {
const p_next = parent.channel_next;
if(p_next) {
this.select([p_next], "exclusive")
return;
}
parent = parent.parent_channel();
}
}
selectNextTreeEntry() {
if(this.selectedEntries.length !== 1) { return; }
const selected = this.selectedEntries[0];
if(selected instanceof ChannelEntry) {
this.selectNextChannel(selected, true);
} else if(selected instanceof ClientEntry){
const channel = selected.currentChannel();
const clients = channel.channelClientsOrdered();
const index = clients.indexOf(selected);
if(index + 1 < clients.length) {
this.select([clients[index + 1]], "exclusive");
return;
}
this.selectNextChannel(channel, false);
} else if(selected instanceof ServerEntry) {
this.select([this.handle.get_first_channel()], "exclusive");
}
}
selectPreviousTreeEntry() {
if(this.selectedEntries.length !== 1) { return; }
const selected = this.selectedEntries[0];
if(selected instanceof ChannelEntry) {
let previous = selected.channel_previous;
if(previous) {
while(true) {
const siblings = previous.children();
if(siblings.length == 0) break;
previous = siblings.last();
}
const clients = previous.channelClientsOrdered();
if(clients.length > 0) {
this.select([ clients.last() ], "exclusive");
return;
} else {
this.select([ previous ], "exclusive");
return;
}
} else if(selected.hasParent()) {
const channel = selected.parent_channel();
const clients = channel.channelClientsOrdered();
if(clients.length > 0) {
this.select([ clients.last() ], "exclusive");
return;
} else {
this.select([ channel ], "exclusive");
return;
}
} else {
this.select([ this.handle.server ], "exclusive");
}
} else if(selected instanceof ClientEntry) {
const channel = selected.currentChannel();
const clients = channel.channelClientsOrdered();
const index = clients.indexOf(selected);
if(index > 0) {
this.select([ clients[index - 1] ], "exclusive");
return;
}
this.select([ channel ], "exclusive");
return;
}
}
private handleSelectEntries(event: ChannelTreeEvents["action_select_entries"]) {
this.select(event.entries, event.mode);
} }
} }
@ -230,7 +356,7 @@ export class ChannelTree {
channels: ChannelEntry[] = []; channels: ChannelEntry[] = [];
clients: ClientEntry[] = []; clients: ClientEntry[] = [];
/* whatever all channels have been initiaized */ /* whatever all channels have been initialized */
channelsInitialized: boolean = false; channelsInitialized: boolean = false;
readonly selection: ChannelTreeEntrySelect; readonly selection: ChannelTreeEntrySelect;
@ -242,10 +368,6 @@ export class ChannelTree {
private channel_last?: ChannelEntry; private channel_last?: ChannelEntry;
private channel_first?: ChannelEntry; private channel_first?: ChannelEntry;
private tagContainerFocused = false;
private listenerDocumentClick;
private listenerDocumentKeyPress;
constructor(client) { constructor(client) {
this.events = new Registry<ChannelTreeEvents>(); this.events = new Registry<ChannelTreeEvents>();
this.events.enableDebug("channel-tree"); this.events.enableDebug("channel-tree");
@ -279,22 +401,6 @@ export class ChannelTree {
}); });
*/ */
} }
/* 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.tagContainer[0]) {
this.tagContainerFocused = true;
break;
}
element = element.parentNode as HTMLElement;
}
};
document.addEventListener('click', this.listenerDocumentClick);
document.addEventListener('keydown', this.listenerDocumentKeyPress);
} }
tag_tree() : JQuery { tag_tree() : JQuery {
@ -336,12 +442,6 @@ export class ChannelTree {
destroy() { destroy() {
ReactDOM.unmountComponentAtNode(this.tagContainer[0]); ReactDOM.unmountComponentAtNode(this.tagContainer[0]);
this.listenerDocumentClick && document.removeEventListener('click', this.listenerDocumentClick);
this.listenerDocumentClick = undefined;
this.listenerDocumentKeyPress && document.removeEventListener('keydown', this.listenerDocumentKeyPress);
this.listenerDocumentKeyPress = undefined;
if(this.server) { if(this.server) {
this.server.destroy(); this.server.destroy();
this.server = undefined; this.server = undefined;
@ -674,7 +774,7 @@ export class ChannelTree {
msg: result msg: result
}); });
this.selection.clear_selection(); this.selection.clearSelection();
} }
}, {width: 400, maxLength: 512}).open(); }, {width: 400, maxLength: 512}).open();
} }
@ -691,7 +791,7 @@ export class ChannelTree {
clid: client.clientId(), clid: client.clientId(),
cid: target cid: target
}); });
this.selection.clear_selection(); this.selection.clearSelection();
} }
}); });
if (!local_client) {//local client cant be kicked and/or banned or kicked if (!local_client) {//local client cant be kicked and/or banned or kicked
@ -711,7 +811,7 @@ export class ChannelTree {
}); });
} }
}, {width: 400, maxLength: 255}).open(); }, {width: 400, maxLength: 255}).open();
this.selection.clear_selection(); this.selection.clearSelection();
} }
}); });
@ -721,7 +821,7 @@ export class ChannelTree {
icon_class: "client-poke", icon_class: "client-poke",
name: tr("Poke clients"), name: tr("Poke clients"),
callback: () => { callback: () => {
this.selection.clear_selection(); this.selection.clearSelection();
createInputModal(tr("Poke clients"), tr("Poke message:<br>"), text => true, result => { createInputModal(tr("Poke clients"), tr("Poke message:<br>"), text => true, result => {
if (result) { if (result) {
const elements = clients.map(e => { return { clid: e.clientId() } as any }); const elements = clients.map(e => { return { clid: e.clientId() } as any });
@ -735,7 +835,7 @@ export class ChannelTree {
icon_class: "client-kick_server", icon_class: "client-kick_server",
name: tr("Kick clients fom server"), name: tr("Kick clients fom server"),
callback: () => { callback: () => {
this.selection.clear_selection(); this.selection.clearSelection();
createInputModal(tr("Kick clients from server"), tr("Kick reason:<br>"), text => true, result => { createInputModal(tr("Kick clients from server"), tr("Kick reason:<br>"), text => true, result => {
if (result) { if (result) {
for (const client of clients) for (const client of clients)
@ -753,7 +853,7 @@ export class ChannelTree {
name: tr("Ban clients"), name: tr("Ban clients"),
invalidPermission: !this.client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).granted(1), invalidPermission: !this.client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).granted(1),
callback: () => { callback: () => {
this.selection.clear_selection(); this.selection.clearSelection();
spawnBanClient(this.client, (clients).map(entry => { spawnBanClient(this.client, (clients).map(entry => {
return { return {
name: entry.clientNickName(), name: entry.clientNickName(),
@ -791,7 +891,7 @@ export class ChannelTree {
this.client.serverConnection.send_command("musicbotdelete", { this.client.serverConnection.send_command("musicbotdelete", {
botid: client.properties.client_database_id botid: client.properties.client_database_id
}); });
this.selection.clear_selection(); this.selection.clearSelection();
} }
}); });
}, },
@ -813,7 +913,7 @@ export class ChannelTree {
if(typeof result === "boolean" && result) { if(typeof result === "boolean" && result) {
for(const channel of channels) for(const channel of channels)
this.client.serverConnection.send_command("channeldelete", { cid: channel.channelId }); this.client.serverConnection.send_command("channeldelete", { cid: channel.channelId });
this.selection.clear_selection(); this.selection.clearSelection();
} }
}); });
} }
@ -879,7 +979,7 @@ export class ChannelTree {
this.channelsInitialized = false; this.channelsInitialized = false;
batch_updates(BatchUpdateType.CHANNEL_TREE); batch_updates(BatchUpdateType.CHANNEL_TREE);
this.selection.clear_selection(); this.selection.clearSelection();
try { try {
this.selection.reset(); this.selection.reset();
@ -944,149 +1044,6 @@ export class ChannelTree {
}); });
} }
private select_next_channel(channel: ChannelEntry, select_client: boolean) {
if(select_client) {
const clients = channel.channelClientsOrdered();
if(clients.length > 0) {
this.events.fire("action_select_entries", {
mode: "exclusive",
entries: [ clients[0] ]
});
return;
}
}
const children = channel.children();
if(children.length > 0) {
this.events.fire("action_select_entries", {
mode: "exclusive",
entries: [ children[0] ]
});
return;
}
const next = channel.channel_next;
if(next) {
this.events.fire("action_select_entries", {
mode: "exclusive",
entries: [ next ]
});
return;
}
let parent = channel.parent_channel();
while(parent) {
const p_next = parent.channel_next;
if(p_next) {
this.events.fire("action_select_entries", {
mode: "exclusive",
entries: [ p_next ]
});
return;
}
parent = parent.parent_channel();
}
}
handle_key_press(event: KeyboardEvent) {
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) {
event.preventDefault();
if(selected instanceof ChannelEntry) {
let previous = selected.channel_previous;
if(previous) {
while(true) {
const siblings = previous.children();
if(siblings.length == 0) break;
previous = siblings.last();
}
const clients = previous.channelClientsOrdered();
if(clients.length > 0) {
this.events.fire("action_select_entries", {
mode: "exclusive",
entries: [ clients.last() ]
});
return;
} else {
this.events.fire("action_select_entries", {
mode: "exclusive",
entries: [ previous ]
});
return;
}
} else if(selected.hasParent()) {
const channel = selected.parent_channel();
const clients = channel.channelClientsOrdered();
if(clients.length > 0) {
this.events.fire("action_select_entries", {
mode: "exclusive",
entries: [ clients.last() ]
});
return;
} else {
this.events.fire("action_select_entries", {
mode: "exclusive",
entries: [ channel ]
});
return;
}
} else {
this.events.fire("action_select_entries", {
mode: "exclusive",
entries: [ this.server ]
});
}
} else if(selected instanceof ClientEntry) {
const channel = selected.currentChannel();
const clients = channel.channelClientsOrdered();
const index = clients.indexOf(selected);
if(index > 0) {
this.events.fire("action_select_entries", {
mode: "exclusive",
entries: [ clients[index - 1] ]
});
return;
}
this.events.fire("action_select_entries", {
mode: "exclusive",
entries: [ channel ]
});
return;
}
} else if(event.keyCode == KeyCode.KEY_DOWN) {
event.preventDefault();
if(selected instanceof ChannelEntry) {
this.select_next_channel(selected, true);
} else if(selected instanceof ClientEntry){
const channel = selected.currentChannel();
const clients = channel.channelClientsOrdered();
const index = clients.indexOf(selected);
if(index + 1 < clients.length) {
this.events.fire("action_select_entries", {
mode: "exclusive",
entries: [ clients[index + 1] ]
});
return;
}
this.select_next_channel(channel, false);
} else if(selected instanceof ServerEntry)
this.events.fire("action_select_entries", {
mode: "exclusive",
entries: [ this.channel_first ]
});
} else if(event.keyCode == KeyCode.KEY_RETURN) {
if(selected instanceof ChannelEntry) {
selected.joinChannel();
}
}
}
toggle_server_queries(flag: boolean) { toggle_server_queries(flag: boolean) {
if(this._show_queries == flag) return; if(this._show_queries == flag) return;
this._show_queries = flag; this._show_queries = flag;
@ -1112,7 +1069,7 @@ export class ChannelTree {
if(channels.length > 0) { if(channels.length > 0) {
this.client.serverConnection.send_command('channelsubscribe', channels.map(e => { return {cid: e}; })).catch(error => { this.client.serverConnection.send_command('channelsubscribe', channels.map(e => { return {cid: e}; })).catch(error => {
console.warn(tr("Failed to subscribe to specific channels (%o)"), channels); console.warn(tr("Failed to subscribe to specific channels (%o): %o"), channels, error);
}); });
} }
}).catch(error => { }).catch(error => {
@ -1133,7 +1090,7 @@ export class ChannelTree {
if(channels.length > 0) { if(channels.length > 0) {
this.client.serverConnection.send_command('channelunsubscribe', channels.map(e => { return {cid: e}; })).catch(error => { this.client.serverConnection.send_command('channelunsubscribe', channels.map(e => { return {cid: e}; })).catch(error => {
console.warn(tr("Failed to unsubscribe to specific channels (%o)"), channels); console.warn(tr("Failed to unsubscribe to specific channels (%o): %o"), channels, error);
}); });
} }
}).catch(error => { }).catch(error => {
@ -1160,56 +1117,4 @@ export class ChannelTree {
this.collapse_channels(child); this.collapse_channels(child);
} }
} }
/*
private onChannelEntryMove(start, current) {
const move = this.view_move.current;
if(!move) return;
const target = this.view.current.getEntryFromPoint(start.x, start.y);
if(target && this.selection.selected_entries.findIndex(e => e === target) === -1)
this.events.fire("action_select_entries", { mode: "auto", entries: [ target ]});
const selection = this.selection.selected_entries;
if(selection.length === 0 || selection.filter(e => !(e instanceof ClientEntry)).length > 0)
return;
move.enableEntryMove(this.view.current, selection.map(e => e as ClientEntry).map(e => e.clientNickName()).join(","), start, current, () => {
this.events.fire("notify_entry_move_begin");
});
}
private onMoveEnd(x: number, y: number) {
batch_updates(BatchUpdateType.CHANNEL_TREE);
try {
this.events.fire("notify_entry_move_end");
const selection = this.selection.selected_entries.filter(e => e instanceof ClientEntry) as ClientEntry[];
if(selection.length === 0) return;
this.selection.clear_selection();
const target = this.view.current.getEntryFromPoint(x, y);
let target_channel: ChannelEntry;
if(target instanceof ClientEntry)
target_channel = target.currentChannel();
else if(target instanceof ChannelEntry)
target_channel = target;
if(!target_channel) return;
selection.filter(e => e.currentChannel() !== target_channel).forEach(e => {
this.client.serverConnection.send_command("clientmove", {
clid: e.clientId(),
cid: target_channel.channelId
});
});
} finally {
flush_batched_updates(BatchUpdateType.CHANNEL_TREE);
}
}
*/
isClientMoveActive() {
//return !!this.view_move.current?.isActive();
return false;
}
} }

View file

@ -657,7 +657,7 @@ export function initializeChannelTreeController(events: Registry<ChannelTreeUIEv
}); });
events.on("action_select", event => { events.on("action_select", event => {
if(!event.ignoreClientMove && channelTree.isClientMoveActive()) { if(!event.ignoreClientMove && moveSelection?.length) {
return; return;
} }
@ -672,10 +672,15 @@ export function initializeChannelTreeController(events: Registry<ChannelTreeUIEv
entries.push(entry); entries.push(entry);
} }
channelTree.events.fire("action_select_entries", { channelTree.selection.select(entries, event.mode);
mode: event.mode, });
entries: entries
}); events.on("action_select_auto", event => {
if(event.direction === "next") {
channelTree.selection.selectNextTreeEntry();
} else if(event.direction === "previous") {
channelTree.selection.selectPreviousTreeEntry();
}
}); });
events.on("action_show_context_menu", event => { events.on("action_show_context_menu", event => {
@ -685,8 +690,8 @@ export function initializeChannelTreeController(events: Registry<ChannelTreeUIEv
return; return;
} }
if (channelTree.selection.is_multi_select() && entry.isSelected()) { if (channelTree.selection.isMultiSelect() && entry.isSelected()) {
channelTree.open_multiselect_context_menu(channelTree.selection.selected_entries, event.pageX, event.pageY); channelTree.open_multiselect_context_menu(channelTree.selection.selectedEntries, event.pageX, event.pageY);
return; return;
} }
@ -698,13 +703,15 @@ export function initializeChannelTreeController(events: Registry<ChannelTreeUIEv
}); });
events.on("action_channel_join", event => { events.on("action_channel_join", event => {
if(!event.ignoreMultiSelect && channelTree.selection.is_multi_select()) { if(!event.ignoreMultiSelect && channelTree.selection.isMultiSelect()) {
return; return;
} }
const entry = channelTree.findEntryId(event.treeEntryId); const entry = event.treeEntryId === "selected" ? channelTree.selection.selectedEntries[0] : channelTree.findEntryId(event.treeEntryId);
if(!entry || !(entry instanceof ChannelEntry)) { if(!entry || !(entry instanceof ChannelEntry)) {
logWarn(LogCategory.CHANNEL, tr("Tried to join an invalid tree entry with id %o"), event.treeEntryId); if(event.treeEntryId !== "selected") {
logWarn(LogCategory.CHANNEL, tr("Tried to join an invalid tree entry with id %o"), event.treeEntryId);
}
return; return;
} }
@ -737,7 +744,7 @@ export function initializeChannelTreeController(events: Registry<ChannelTreeUIEv
return; return;
} }
if(channelTree.selection.is_multi_select()) { if(channelTree.selection.isMultiSelect()) {
return; return;
} }
@ -769,7 +776,7 @@ export function initializeChannelTreeController(events: Registry<ChannelTreeUIEv
let moveSelection: ClientEntry[]; let moveSelection: ClientEntry[];
events.on("action_start_entry_move", event => { events.on("action_start_entry_move", event => {
const selection = channelTree.selection.selected_entries.slice(); const selection = channelTree.selection.selectedEntries.slice();
if(selection.length === 0) { return; } if(selection.length === 0) { return; }
if(selection.findIndex(element => !(element instanceof ClientEntry)) !== -1) { return; } if(selection.findIndex(element => !(element instanceof ClientEntry)) !== -1) { return; }

View file

@ -37,7 +37,8 @@ export interface ChannelTreeUIEvents {
mode: "auto" | "exclusive" | "append" | "remove", mode: "auto" | "exclusive" | "append" | "remove",
ignoreClientMove: boolean ignoreClientMove: boolean
}, },
action_channel_join: { treeEntryId: number, ignoreMultiSelect: boolean }, action_select_auto: { direction: "next" | "previous" },
action_channel_join: { treeEntryId: number | "selected", ignoreMultiSelect: boolean },
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 },

View file

@ -1,13 +1,59 @@
import {Registry} from "tc-shared/events"; import {Registry} from "tc-shared/events";
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions"; import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
import * as React from "react"; import * as React from "react";
import {ChannelTreeView} from "tc-shared/ui/tree/RendererView"; import {ChannelTreeView, PopoutButton} from "tc-shared/ui/tree/RendererView";
import {RDPChannelTree} from "./RendererDataProvider"; import {RDPChannelTree} from "./RendererDataProvider";
import {useEffect} from "react"; import {useEffect, useRef} from "react";
const viewStyle = require("./View.scss");
export const ChannelTreeRenderer = (props: { handlerId: string, events: Registry<ChannelTreeUIEvents> }) => { export const ChannelTreeRenderer = (props: { handlerId: string, events: Registry<ChannelTreeUIEvents> }) => {
const dataProvider = new RDPChannelTree(props.events, props.handlerId); const dataProvider = new RDPChannelTree(props.events, props.handlerId);
dataProvider.initialize(); dataProvider.initialize();
useEffect(() => () => dataProvider.destroy()); useEffect(() => () => dataProvider.destroy());
return <ChannelTreeView events={props.events} dataProvider={dataProvider} ref={dataProvider.refTree} />;
return <ContainerView tree={dataProvider} events={props.events} />;
}
const ContainerView = (props: { tree: RDPChannelTree, events: Registry<ChannelTreeUIEvents> }) => {
const refContainer = useRef<HTMLDivElement>();
const focusWithin = useRef(false);
useEffect(() => {
let mouseDownListener;
document.addEventListener("mousedown", mouseDownListener = event => {
let target = event.target as HTMLElement;
while(target !== refContainer.current && target) { target = target.parentElement; }
focusWithin.current = !!target;
});
let keyListener;
document.addEventListener("keydown", keyListener = event => {
if(!focusWithin.current) { return; }
if(event.key === "ArrowUp") {
event.preventDefault();
props.events.fire("action_select_auto", { direction: "previous" });
} else if(event.key === "ArrowDown") {
event.preventDefault();
props.events.fire("action_select_auto", { direction: "next" });
} else if(event.key === "Enter") {
event.preventDefault();
props.events.fire("action_channel_join", { treeEntryId: "selected", ignoreMultiSelect: false });
}
});
return () => {
document.removeEventListener("mousedown", mouseDownListener);
document.removeEventListener("keypress", keyListener);
}
});
return (
<div className={viewStyle.treeContainer} ref={refContainer}>
<ChannelTreeView events={props.events} dataProvider={props.tree} ref={props.tree.refTree} />
<PopoutButton tree={props.tree} ref={props.tree.refPopoutButton} />
</div>
)
} }

View file

@ -257,7 +257,7 @@ export class RDPChannelTree {
entry.destroy(); entry.destroy();
}); });
this.refTree?.current.setState({ this.refTree.current?.setState({
tree: this.orderedTree.slice(), tree: this.orderedTree.slice(),
treeRevision: ++this.treeRevision treeRevision: ++this.treeRevision
}); });

View file

@ -186,30 +186,27 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
} }
return ( return (
<div className={viewStyle.treeContainer}> <div
className={viewStyle.channelTreeContainer + " " + (this.state.smoothScroll ? viewStyle.smoothScroll : "")}
onScroll={() => this.onScroll()}
ref={this.refContainer}
onMouseDown={e => this.onMouseDown(e)}
onMouseMove={e => this.onMouseMove(e)}>
<div <div
className={viewStyle.channelTreeContainer + " " + (this.state.smoothScroll ? viewStyle.smoothScroll : "")} className={viewStyle.channelTree}
onScroll={() => this.onScroll()} style={{height: (this.state.tree.length * ChannelTreeView.EntryHeight) + "px"}}>
ref={this.refContainer} {elements}
onMouseDown={e => this.onMouseDown(e)}
onMouseMove={e => this.onMouseMove(e)}>
<div
className={viewStyle.channelTree}
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> </div>
<PopoutButton tree={this.props.dataProvider} ref={this.props.dataProvider.refPopoutButton} /> <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>
) )
} }