Implementing key tree control for the external channel trees
This commit is contained in:
parent
9ab430e8db
commit
883ccbbc65
6 changed files with 285 additions and 329 deletions
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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; }
|
||||||
|
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
}
|
}
|
|
@ -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
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue