Added channel movement and reordering
parent
9a42a02227
commit
903c29ac51
|
@ -2,6 +2,7 @@
|
|||
* **03.12.20**
|
||||
- Fixed server connection tab move handler
|
||||
- Fixed file browser context menu when having an overlay
|
||||
- Added channel movement and reordering
|
||||
|
||||
* **02.12.20**
|
||||
- Fixed a bug within the client entry move mechanic of the channel tree which prevented any client selection
|
||||
|
|
|
@ -342,16 +342,6 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
return this._family_index;
|
||||
}
|
||||
|
||||
protected onSelect(singleSelect: boolean) {
|
||||
super.onSelect(singleSelect);
|
||||
if(!singleSelect) return;
|
||||
|
||||
if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) {
|
||||
this.channelTree.client.side_bar.channel_conversations().setSelectedConversation(this.channelId);
|
||||
this.channelTree.client.side_bar.show_channel_conversations();
|
||||
}
|
||||
}
|
||||
|
||||
showContextMenu(x: number, y: number, on_close: () => void = undefined) {
|
||||
let channelCreate = !![
|
||||
PermissionType.B_CHANNEL_CREATE_TEMPORARY,
|
||||
|
|
|
@ -26,22 +26,11 @@ import {tra} from "tc-shared/i18n/localize";
|
|||
import {EventType} from "tc-shared/ui/frames/log/Definitions";
|
||||
import {renderChannelTree} from "tc-shared/ui/tree/Controller";
|
||||
import {ChannelTreePopoutController} from "tc-shared/ui/tree/popout/Controller";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
|
||||
export interface ChannelTreeEvents {
|
||||
action_select_entries: {
|
||||
entries: ChannelTreeEntry<any>[],
|
||||
/**
|
||||
* auto := Select/unselect/add/remove depending on the selected state & shift key state
|
||||
* exclusive := Only selected these entries
|
||||
* append := Append these entries to the current selection
|
||||
* remove := Remove these entries from the current selection
|
||||
*/
|
||||
mode: "auto" | "exclusive" | "append" | "remove";
|
||||
},
|
||||
|
||||
/* general tree notified */
|
||||
notify_tree_reset: {},
|
||||
notify_selection_changed: {},
|
||||
notify_query_view_state_changed: { queries_shown: boolean },
|
||||
notify_popout_state_changed: { popoutShown: boolean },
|
||||
|
||||
|
@ -80,270 +69,11 @@ export interface ChannelTreeEvents {
|
|||
message?: string,
|
||||
isServerLeave: boolean,
|
||||
sourceChannel: ChannelEntry
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
export class ChannelTreeEntrySelect {
|
||||
readonly handle: ChannelTree;
|
||||
selectedEntries: ChannelTreeEntry<any>[] = [];
|
||||
|
||||
private readonly handlerSelectEntries;
|
||||
|
||||
constructor(handle: ChannelTree) {
|
||||
this.handle = handle;
|
||||
|
||||
this.handlerSelectEntries = e => {
|
||||
batch_updates(BatchUpdateType.CHANNEL_TREE);
|
||||
try {
|
||||
this.handleSelectEntries(e)
|
||||
} finally {
|
||||
flush_batched_updates(BatchUpdateType.CHANNEL_TREE);
|
||||
}
|
||||
};
|
||||
|
||||
this.handle.events.on("action_select_entries", this.handlerSelectEntries);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.selectedEntries.splice(0, this.selectedEntries.length);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.handle.events.off("action_select_entries", this.handlerSelectEntries);
|
||||
this.selectedEntries.splice(0, this.selectedEntries.length);
|
||||
}
|
||||
|
||||
isMultiSelect() {
|
||||
return this.selectedEntries.length > 1;
|
||||
}
|
||||
|
||||
isAnythingSelected() {
|
||||
return this.selectedEntries.length > 0;
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this.handleSelectEntries({
|
||||
entries: [],
|
||||
mode: "exclusive"
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* auto := Select/unselect/add/remove depending on the selected state & shift key state
|
||||
* 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" | "auto-add" | "exclusive" | "append" | "remove") {
|
||||
entries = entries.filter(entry => !!entry);
|
||||
|
||||
if(mode === "exclusive") {
|
||||
let deleted_entries = this.selectedEntries;
|
||||
let new_entries = [];
|
||||
|
||||
this.selectedEntries = [];
|
||||
for(const new_entry of entries) {
|
||||
if(!deleted_entries.remove(new_entry)) {
|
||||
new_entries.push(new_entry);
|
||||
}
|
||||
|
||||
this.selectedEntries.push(new_entry);
|
||||
}
|
||||
|
||||
for(const deleted of deleted_entries) {
|
||||
deleted["onUnselect"]();
|
||||
}
|
||||
|
||||
for(const new_entry of new_entries) {
|
||||
new_entry["onSelect"](!this.isMultiSelect());
|
||||
}
|
||||
|
||||
if(deleted_entries.length !== 0 || new_entries.length !== 0) {
|
||||
this.handle.events.fire("notify_selection_changed");
|
||||
}
|
||||
} else if(mode === "append") {
|
||||
let new_entries = [];
|
||||
for(const entry of entries) {
|
||||
if(this.selectedEntries.findIndex(e => e === entry) !== -1)
|
||||
continue;
|
||||
|
||||
this.selectedEntries.push(entry);
|
||||
new_entries.push(entry);
|
||||
}
|
||||
|
||||
for(const new_entry of new_entries) {
|
||||
new_entry["onSelect"](!this.isMultiSelect());
|
||||
}
|
||||
|
||||
if(new_entries.length !== 0) {
|
||||
this.handle.events.fire("notify_selection_changed");
|
||||
}
|
||||
} 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" || mode === "auto-add") {
|
||||
let deleted_entries = [];
|
||||
let new_entries = [];
|
||||
|
||||
if(ppt.key_pressed(SpecialKey.SHIFT)) {
|
||||
for(const entry of entries) {
|
||||
const index = this.selectedEntries.findIndex(e => e === entry);
|
||||
if(index === -1) {
|
||||
this.selectedEntries.push(entry);
|
||||
new_entries.push(entry);
|
||||
} else if(mode === "auto") {
|
||||
this.selectedEntries.splice(index, 1);
|
||||
deleted_entries.push(entry);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
deleted_entries = this.selectedEntries.splice(0, this.selectedEntries.length);
|
||||
if(entries.length !== 0) {
|
||||
const entry = entries[entries.length - 1];
|
||||
this.selectedEntries.push(entry);
|
||||
if(!deleted_entries.remove(entry)) {
|
||||
new_entries.push(entry); /* entry wans't selected yet */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(const deleted of deleted_entries) {
|
||||
deleted["onUnselect"]();
|
||||
}
|
||||
|
||||
for(const new_entry of new_entries) {
|
||||
new_entry["onSelect"](!this.isMultiSelect());
|
||||
}
|
||||
|
||||
if(deleted_entries.length !== 0 || new_entries.length !== 0) {
|
||||
this.handle.events.fire("notify_selection_changed");
|
||||
}
|
||||
} else {
|
||||
console.warn("Received entry select event with unknown mode: %s", mode);
|
||||
}
|
||||
|
||||
/*
|
||||
TODO!
|
||||
if(this.selected_entries.length === 1)
|
||||
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);
|
||||
notify_selected_entry_changed: {
|
||||
oldEntry: ChannelTreeEntry<any> | undefined,
|
||||
newEntry: ChannelTreeEntry<any> | undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -359,14 +89,14 @@ export class ChannelTree {
|
|||
/* whatever all channels have been initialized */
|
||||
channelsInitialized: boolean = false;
|
||||
|
||||
readonly selection: ChannelTreeEntrySelect;
|
||||
readonly popoutController: ChannelTreePopoutController;
|
||||
|
||||
private readonly tagContainer: JQuery;
|
||||
|
||||
private _show_queries: boolean;
|
||||
private channel_last?: ChannelEntry;
|
||||
private channel_first?: ChannelEntry;
|
||||
private selectedEntry: ChannelTreeEntry<any> | undefined;
|
||||
private showQueries: boolean;
|
||||
private channelLast?: ChannelEntry;
|
||||
private channelFirst?: ChannelEntry;
|
||||
|
||||
constructor(client) {
|
||||
this.events = new Registry<ChannelTreeEvents>();
|
||||
|
@ -375,7 +105,6 @@ export class ChannelTree {
|
|||
this.client = client;
|
||||
|
||||
this.server = new ServerEntry(this, "undefined", undefined);
|
||||
this.selection = new ChannelTreeEntrySelect(this);
|
||||
this.popoutController = new ChannelTreePopoutController(this);
|
||||
|
||||
this.tagContainer = $.spawn("div").addClass("channel-tree-container");
|
||||
|
@ -396,7 +125,7 @@ export class ChannelTree {
|
|||
channel.child_channel_head && visit(channel.child_channel_head);
|
||||
channel.channel_next && visit(channel.channel_next);
|
||||
};
|
||||
this.channel_first && visit(this.channel_first);
|
||||
this.channelFirst && visit(this.channelFirst);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -420,6 +149,40 @@ export class ChannelTree {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
getSelectedEntry() : ChannelTreeEntry<any> | undefined {
|
||||
return this.selectedEntry;
|
||||
}
|
||||
|
||||
setSelectedEntry(entry: ChannelTreeEntry<any> | undefined) {
|
||||
if(this.selectedEntry === entry) { return; }
|
||||
|
||||
const oldEntry = this.selectedEntry;
|
||||
this.selectedEntry = entry;
|
||||
this.events.fire("notify_selected_entry_changed", { newEntry: entry, oldEntry: oldEntry });
|
||||
|
||||
if(this.selectedEntry instanceof ClientEntry) {
|
||||
if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CLIENT)) {
|
||||
if(this.selectedEntry instanceof MusicClientEntry) {
|
||||
this.client.side_bar.show_music_player(this.selectedEntry);
|
||||
} else {
|
||||
this.client.side_bar.show_client_info(this.selectedEntry);
|
||||
}
|
||||
}
|
||||
} else if(this.selectedEntry instanceof ChannelEntry) {
|
||||
if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) {
|
||||
this.client.side_bar.channel_conversations().setSelectedConversation(this.selectedEntry.channelId);
|
||||
this.client.side_bar.show_channel_conversations();
|
||||
}
|
||||
} else if(this.selectedEntry instanceof ServerEntry) {
|
||||
if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) {
|
||||
const sidebar = this.client.side_bar;
|
||||
sidebar.channel_conversations().findOrCreateConversation(0);
|
||||
sidebar.channel_conversations().setSelectedConversation(0);
|
||||
sidebar.show_channel_conversations();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
ReactDOM.unmountComponentAtNode(this.tagContainer[0]);
|
||||
|
||||
|
@ -429,12 +192,11 @@ export class ChannelTree {
|
|||
}
|
||||
this.reset(); /* cleanup channel and clients */
|
||||
|
||||
this.channel_first = undefined;
|
||||
this.channel_last = undefined;
|
||||
this.channelFirst = undefined;
|
||||
this.channelLast = undefined;
|
||||
|
||||
this.popoutController.destroy();
|
||||
this.tagContainer.remove();
|
||||
this.selection.destroy();
|
||||
this.events.destroy();
|
||||
}
|
||||
|
||||
|
@ -446,7 +208,7 @@ export class ChannelTree {
|
|||
|
||||
rootChannel() : ChannelEntry[] {
|
||||
const result = [];
|
||||
let first = this.channel_first;
|
||||
let first = this.channelFirst;
|
||||
while(first) {
|
||||
result.push(first);
|
||||
first = first.channel_next;
|
||||
|
@ -455,6 +217,10 @@ export class ChannelTree {
|
|||
}
|
||||
|
||||
deleteChannel(channel: ChannelEntry) {
|
||||
if(this.selectedEntry === channel) {
|
||||
this.setSelectedEntry(undefined);
|
||||
}
|
||||
|
||||
channel.channelTree = null;
|
||||
|
||||
batch_updates(BatchUpdateType.CHANNEL_TREE);
|
||||
|
@ -517,12 +283,12 @@ export class ChannelTree {
|
|||
channel.channel_next.channel_previous = channel.channel_previous;
|
||||
}
|
||||
|
||||
if(channel === this.channel_last) {
|
||||
this.channel_last = channel.channel_previous;
|
||||
if(channel === this.channelLast) {
|
||||
this.channelLast = channel.channel_previous;
|
||||
}
|
||||
|
||||
if(channel === this.channel_first) {
|
||||
this.channel_first = channel.channel_next;
|
||||
if(channel === this.channelFirst) {
|
||||
this.channelFirst = channel.channel_next;
|
||||
}
|
||||
|
||||
channel.channel_next = undefined;
|
||||
|
@ -542,8 +308,8 @@ export class ChannelTree {
|
|||
channel.parent = parent;
|
||||
|
||||
if(channelPrevious) {
|
||||
if(channelPrevious == this.channel_last) {
|
||||
this.channel_last = channel;
|
||||
if(channelPrevious == this.channelLast) {
|
||||
this.channelLast = channel;
|
||||
}
|
||||
|
||||
channel.channel_next = channelPrevious.channel_next;
|
||||
|
@ -563,12 +329,13 @@ export class ChannelTree {
|
|||
channel.channel_next.channel_previous = channel;
|
||||
}
|
||||
} else {
|
||||
channel.channel_next = this.channel_first;
|
||||
if(this.channel_first)
|
||||
this.channel_first.channel_previous = channel;
|
||||
channel.channel_next = this.channelFirst;
|
||||
if(this.channelFirst) {
|
||||
this.channelFirst.channel_previous = channel;
|
||||
}
|
||||
|
||||
this.channel_first = channel;
|
||||
this.channel_last = this.channel_last || channel;
|
||||
this.channelFirst = channel;
|
||||
this.channelLast = this.channelLast || channel;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -587,6 +354,10 @@ export class ChannelTree {
|
|||
}
|
||||
|
||||
deleteClient(client: ClientEntry, reason: { reason: ViewReasonId, message?: string, serverLeave: boolean }) {
|
||||
if(this.selectedEntry === client) {
|
||||
this.setSelectedEntry(undefined);
|
||||
}
|
||||
|
||||
const oldChannel = client.currentChannel();
|
||||
oldChannel?.unregisterClient(client);
|
||||
this.unregisterClient(client);
|
||||
|
@ -747,7 +518,7 @@ export class ChannelTree {
|
|||
);
|
||||
}
|
||||
|
||||
public open_multiselect_context_menu(entries: ChannelTreeEntry<any>[], x: number, y: number) {
|
||||
public showMultiSelectContextMenu(entries: ChannelTreeEntry<any>[], x: number, y: number) {
|
||||
const clients = entries.filter(e => e instanceof ClientEntry) as ClientEntry[];
|
||||
const channels = entries.filter(e => e instanceof ChannelEntry) as ChannelEntry[];
|
||||
const server = entries.find(e => e instanceof ServerEntry) as ServerEntry;
|
||||
|
@ -771,13 +542,12 @@ export class ChannelTree {
|
|||
callback: () => {
|
||||
createInputModal(tr("Poke clients"), tr("Poke message:<br>"), text => true, result => {
|
||||
if (typeof(result) === "string") {
|
||||
for (const client of clients)
|
||||
for (const client of clients) {
|
||||
this.client.serverConnection.send_command("clientpoke", {
|
||||
clid: client.clientId(),
|
||||
msg: result
|
||||
});
|
||||
|
||||
this.selection.clearSelection();
|
||||
}
|
||||
}
|
||||
}, {width: 400, maxLength: 512}).open();
|
||||
}
|
||||
|
@ -789,12 +559,12 @@ export class ChannelTree {
|
|||
name: tr("Move clients to your channel"),
|
||||
callback: () => {
|
||||
const target = this.client.getClient().currentChannel().getChannelId();
|
||||
for(const client of clients)
|
||||
for(const client of clients) {
|
||||
this.client.serverConnection.send_command("clientmove", {
|
||||
clid: client.clientId(),
|
||||
cid: target
|
||||
});
|
||||
this.selection.clearSelection();
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!local_client) {//local client cant be kicked and/or banned or kicked
|
||||
|
@ -814,7 +584,6 @@ export class ChannelTree {
|
|||
});
|
||||
}
|
||||
}, {width: 400, maxLength: 255}).open();
|
||||
this.selection.clearSelection();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -824,7 +593,6 @@ export class ChannelTree {
|
|||
icon_class: "client-poke",
|
||||
name: tr("Poke clients"),
|
||||
callback: () => {
|
||||
this.selection.clearSelection();
|
||||
createInputModal(tr("Poke clients"), tr("Poke message:<br>"), text => true, result => {
|
||||
if (result) {
|
||||
const elements = clients.map(e => { return { clid: e.clientId() } as any });
|
||||
|
@ -838,7 +606,6 @@ export class ChannelTree {
|
|||
icon_class: "client-kick_server",
|
||||
name: tr("Kick clients fom server"),
|
||||
callback: () => {
|
||||
this.selection.clearSelection();
|
||||
createInputModal(tr("Kick clients from server"), tr("Kick reason:<br>"), text => true, result => {
|
||||
if (result) {
|
||||
for (const client of clients)
|
||||
|
@ -856,7 +623,6 @@ export class ChannelTree {
|
|||
name: tr("Ban clients"),
|
||||
invalidPermission: !this.client.permissions.neededPermission(PermissionType.I_CLIENT_BAN_MAX_BANTIME).granted(1),
|
||||
callback: () => {
|
||||
this.selection.clearSelection();
|
||||
spawnBanClient(this.client, (clients).map(entry => {
|
||||
return {
|
||||
name: entry.clientNickName(),
|
||||
|
@ -890,11 +656,11 @@ export class ChannelTree {
|
|||
const tag_container = $.spawn("div").append(tag);
|
||||
spawnYesNo(tr("Are you sure?"), tag_container, result => {
|
||||
if(result) {
|
||||
for(const client of clients)
|
||||
for(const client of clients) {
|
||||
this.client.serverConnection.send_command("musicbotdelete", {
|
||||
botid: client.properties.client_database_id
|
||||
});
|
||||
this.selection.clearSelection();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -914,16 +680,17 @@ export class ChannelTree {
|
|||
callback: () => {
|
||||
spawnYesNo(tr("Are you sure?"), tra("Do you really want to delete {0} channels?", channels.length), 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.selection.clearSelection();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if(server)
|
||||
if(server) {
|
||||
server_menu = server.contextMenuItems();
|
||||
}
|
||||
|
||||
const menus = [
|
||||
{
|
||||
|
@ -982,9 +749,8 @@ export class ChannelTree {
|
|||
this.channelsInitialized = false;
|
||||
batch_updates(BatchUpdateType.CHANNEL_TREE);
|
||||
|
||||
this.selection.clearSelection();
|
||||
try {
|
||||
this.selection.reset();
|
||||
this.setSelectedEntry(undefined);
|
||||
|
||||
const voiceConnection = this.client.serverConnection ? this.client.serverConnection.getVoiceConnection() : undefined;
|
||||
const videoConnection = this.client.serverConnection ? this.client.serverConnection.getVideoConnection() : undefined;
|
||||
|
@ -1006,8 +772,8 @@ export class ChannelTree {
|
|||
channel.destroy();
|
||||
|
||||
this.channels = [];
|
||||
this.channel_last = undefined;
|
||||
this.channel_first = undefined;
|
||||
this.channelLast = undefined;
|
||||
this.channelFirst = undefined;
|
||||
this.events.fire("notify_tree_reset");
|
||||
} finally {
|
||||
flush_batched_updates(BatchUpdateType.CHANNEL_TREE);
|
||||
|
@ -1054,15 +820,15 @@ export class ChannelTree {
|
|||
}
|
||||
|
||||
toggle_server_queries(flag: boolean) {
|
||||
if(this._show_queries == flag) return;
|
||||
this._show_queries = flag;
|
||||
if(this.showQueries == flag) return;
|
||||
this.showQueries = flag;
|
||||
|
||||
this.events.fire("notify_query_view_state_changed", { queries_shown: flag });
|
||||
}
|
||||
areServerQueriesShown() { return this._show_queries; }
|
||||
areServerQueriesShown() { return this.showQueries; }
|
||||
|
||||
get_first_channel?() : ChannelEntry {
|
||||
return this.channel_first;
|
||||
return this.channelFirst;
|
||||
}
|
||||
|
||||
unsubscribe_all_channels(subscribe_specified?: boolean) {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import {Registry} from "../events";
|
||||
|
||||
export interface ChannelTreeEntryEvents {
|
||||
notify_select_state_change: { selected: boolean },
|
||||
notify_unread_state_change: { unread: boolean }
|
||||
}
|
||||
|
||||
|
@ -17,24 +16,6 @@ export abstract class ChannelTreeEntry<Events extends ChannelTreeEntryEvents> {
|
|||
this.uniqueEntryId = ++treeEntryIdCounter;
|
||||
}
|
||||
|
||||
/* called from the channel tree */
|
||||
protected onSelect(singleSelect: boolean) {
|
||||
if(this.selected_ === true) return;
|
||||
this.selected_ = true;
|
||||
|
||||
this.events.fire("notify_select_state_change", { selected: true });
|
||||
}
|
||||
|
||||
/* called from the channel tree */
|
||||
protected onUnselect() {
|
||||
if(this.selected_ === false) return;
|
||||
this.selected_ = false;
|
||||
|
||||
this.events.fire("notify_select_state_change", { selected: false });
|
||||
}
|
||||
|
||||
isSelected() { return this.selected_; }
|
||||
|
||||
setUnread(flag: boolean) {
|
||||
if(this.unread_ === flag) return;
|
||||
this.unread_ = flag;
|
||||
|
|
|
@ -372,56 +372,6 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
|||
protected initializeListener() {
|
||||
if(this._listener_initialized) return;
|
||||
this._listener_initialized = true;
|
||||
|
||||
//FIXME: TODO!
|
||||
/*
|
||||
this.tag.on('mousedown', event => {
|
||||
if(event.which != 1) return; //Only the left button
|
||||
|
||||
let clients = this.channelTree.currently_selected as (ClientEntry | ClientEntry[]);
|
||||
|
||||
if(ppt.key_pressed(SpecialKey.SHIFT)) {
|
||||
if(clients != this && !($.isArray(clients) && clients.indexOf(this) != -1))
|
||||
clients = $.isArray(clients) ? [...clients, this] : [clients, this];
|
||||
} else {
|
||||
clients = this;
|
||||
}
|
||||
|
||||
this.channelTree.client_mover.activate(clients, target => {
|
||||
if(!target) return;
|
||||
|
||||
for(const client of $.isArray(clients) ? clients : [clients]) {
|
||||
if(target == client._channel) continue;
|
||||
|
||||
const source = client._channel;
|
||||
const self = this.channelTree.client.getClient();
|
||||
this.channelTree.client.serverConnection.send_command("clientmove", {
|
||||
clid: client.clientId(),
|
||||
cid: target.getChannelId()
|
||||
}).then(event => {
|
||||
if(client.clientId() == this.channelTree.client.clientId)
|
||||
this.channelTree.client.sound.play(Sound.CHANNEL_JOINED);
|
||||
else if(target !== source && target != self.currentChannel())
|
||||
this.channelTree.client.sound.play(Sound.USER_MOVED);
|
||||
});
|
||||
}
|
||||
|
||||
this.channelTree.onSelect();
|
||||
}, event);
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
protected onSelect(singleSelect: boolean) {
|
||||
super.onSelect(singleSelect);
|
||||
if(!singleSelect) return;
|
||||
|
||||
if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CLIENT)) {
|
||||
if(this instanceof MusicClientEntry)
|
||||
this.channelTree.client.side_bar.show_music_player(this);
|
||||
else
|
||||
this.channelTree.client.side_bar.show_client_info(this);
|
||||
}
|
||||
}
|
||||
|
||||
protected contextmenu_info() : contextmenu.MenuEntry[] {
|
||||
|
|
|
@ -170,18 +170,6 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
|
|||
this.remote_address = undefined;
|
||||
}
|
||||
|
||||
protected onSelect(singleSelect: boolean) {
|
||||
super.onSelect(singleSelect);
|
||||
if(!singleSelect) return;
|
||||
|
||||
if(settings.static_global(Settings.KEY_SWITCH_INSTANT_CHAT)) {
|
||||
const sidebar = this.channelTree.client.side_bar;
|
||||
sidebar.channel_conversations().findOrCreateConversation(0);
|
||||
sidebar.channel_conversations().setSelectedConversation(0);
|
||||
sidebar.show_channel_conversations();
|
||||
}
|
||||
}
|
||||
|
||||
contextMenuItems() : contextmenu.MenuEntry[] {
|
||||
return [
|
||||
{
|
||||
|
|
|
@ -4,12 +4,12 @@ import {FileType} from "../../../file/FileManager";
|
|||
import {CommandResult} from "../../../connection/ServerConnectionDeclaration";
|
||||
import PermissionType from "../../../permission/PermissionType";
|
||||
import * as log from "../../../log";
|
||||
import {LogCategory} from "../../../log";
|
||||
import {LogCategory, logTrace} from "../../../log";
|
||||
import {Entry, MenuEntry, MenuEntryType, spawn_context_menu} from "../../../ui/elements/ContextMenu";
|
||||
import * as ppt from "tc-backend/ppt";
|
||||
import {SpecialKey} from "../../../PPTListener";
|
||||
import {spawnYesNo} from "../../../ui/modal/ModalYesNo";
|
||||
import {tra, traj, tr} from "../../../i18n/localize";
|
||||
import {tr, tra, traj} from "../../../i18n/localize";
|
||||
import {
|
||||
FileTransfer,
|
||||
FileTransferState,
|
||||
|
@ -105,6 +105,8 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
|
|||
});
|
||||
return;
|
||||
}
|
||||
logTrace(LogCategory.FILE_TRANSFER, tr("Requesting a file list for %o"), path);
|
||||
|
||||
let request: Promise<ListedFileInfo[]>;
|
||||
if (path.type === "root") {
|
||||
request = (async () => {
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
html:root {
|
||||
--channel-tree-move-border: #005fa1;
|
||||
}
|
||||
|
||||
.channelEntry {
|
||||
position: relative;
|
||||
|
||||
|
@ -110,4 +114,58 @@
|
|||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
height: 18px + 2px!important;
|
||||
|
||||
margin-top: -1px!important;
|
||||
padding-top: 1px!important;
|
||||
|
||||
margin-bottom: -1px!important;
|
||||
padding-bottom: 1px!important;
|
||||
|
||||
padding-left: 1px!important;
|
||||
|
||||
.leftPadding {
|
||||
height: 100%;
|
||||
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
&.drag-top {
|
||||
height: 20px!important;
|
||||
|
||||
padding-top: 0!important;
|
||||
padding-bottom: 2px!important;
|
||||
|
||||
border-top: 2px solid var(--channel-tree-move-border);
|
||||
|
||||
.leftPadding {
|
||||
background-color: var(--channel-tree-background);
|
||||
margin-top: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
&.drag-bottom {
|
||||
padding-bottom: 0!important;
|
||||
border-bottom: 2px solid var(--channel-tree-move-border);
|
||||
|
||||
z-index: 1;
|
||||
|
||||
.leftPadding {
|
||||
background-color: var(--channel-tree-background);
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
&.drag-contain {
|
||||
padding-top: 0!important;
|
||||
padding-bottom: 0!important;
|
||||
|
||||
padding-left: 0!important;
|
||||
border: 1px solid var(--channel-tree-move-border);
|
||||
|
||||
border-radius: 2px;
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
|
@ -196,6 +196,7 @@ class ChannelTreeController {
|
|||
this.channelTree.channels.forEach(channel => this.initializeChannelEvents(channel));
|
||||
this.channelTree.clients.forEach(channel => this.initializeClientEvents(channel));
|
||||
this.sendChannelTreeEntries();
|
||||
this.sendSelectedEntry();
|
||||
}
|
||||
|
||||
@EventHandler<ChannelTreeEvents>("notify_channel_created")
|
||||
|
@ -250,15 +251,18 @@ class ChannelTreeController {
|
|||
this.sendChannelTreeEntries();
|
||||
}
|
||||
|
||||
@EventHandler<ChannelTreeEvents>("notify_selected_entry_changed")
|
||||
private handleSelectedEntryChanged(_event: ChannelTreeEvents["notify_selected_entry_changed"]) {
|
||||
if(!this.channelTreeInitialized) { return; }
|
||||
|
||||
this.sendSelectedEntry();
|
||||
}
|
||||
|
||||
/* entry event handlers */
|
||||
private initializeTreeEntryEvents<T extends ChannelTreeEntryEvents>(entry: ChannelTreeEntryModel<T>, events: any[]) {
|
||||
events.push(entry.events.on("notify_unread_state_change", event => {
|
||||
this.events.fire("notify_unread_state", { unread: event.unread, treeEntryId: entry.uniqueEntryId });
|
||||
}));
|
||||
|
||||
events.push(entry.events.on("notify_select_state_change", event => {
|
||||
this.events.fire("notify_select_state", { selected: event.selected, treeEntryId: entry.uniqueEntryId });
|
||||
}));
|
||||
}
|
||||
|
||||
private initializeChannelEvents(channel: ChannelEntry) {
|
||||
|
@ -387,6 +391,11 @@ class ChannelTreeController {
|
|||
this.events.fire_react("notify_tree_entries", { entries: entries });
|
||||
}
|
||||
|
||||
public sendSelectedEntry() {
|
||||
const selectedEntry = this.channelTree.getSelectedEntry();
|
||||
this.events.fire_react("notify_selected_entry", { treeEntryId: selectedEntry ? selectedEntry.uniqueEntryId : 0 });
|
||||
}
|
||||
|
||||
public sendChannelInfo(channel: ChannelEntry) {
|
||||
this.events.fire_react("notify_channel_info", {
|
||||
treeEntryId: channel.uniqueEntryId,
|
||||
|
@ -558,16 +567,6 @@ export function initializeChannelTreeController(events: Registry<ChannelTreeUIEv
|
|||
events.fire_react("notify_unread_state", { treeEntryId: event.treeEntryId, unread: entry.isUnread() });
|
||||
});
|
||||
|
||||
events.on("query_select_state", event => {
|
||||
const entry = channelTree.findEntryId(event.treeEntryId);
|
||||
if(!entry) {
|
||||
logWarn(LogCategory.CHANNEL, tr("Tried to query the select state of an invalid tree entry with id %o"), event.treeEntryId);
|
||||
return;
|
||||
}
|
||||
|
||||
events.fire_react("notify_select_state", { treeEntryId: event.treeEntryId, selected: entry.isSelected() });
|
||||
});
|
||||
|
||||
events.on("notify_destroy", channelTree.client.events().on("notify_visibility_changed", event => events.fire("notify_visibility_changed", event)));
|
||||
|
||||
events.on("query_tree_entries", () => controller.sendChannelTreeEntries());
|
||||
|
@ -663,67 +662,43 @@ export function initializeChannelTreeController(events: Registry<ChannelTreeUIEv
|
|||
});
|
||||
|
||||
events.on("action_select", event => {
|
||||
console.error("Select mode: %o", moveSelection);
|
||||
if(!event.ignoreClientMove && moveSelection?.length) {
|
||||
console.error("X");
|
||||
if(event.treeEntryId === 0) {
|
||||
channelTree.setSelectedEntry(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = [];
|
||||
for(const entryId of event.entryIds) {
|
||||
const entry = channelTree.findEntryId(entryId);
|
||||
if(!entry) {
|
||||
logWarn(LogCategory.CHANNEL, tr("Tried to select an invalid tree entry with id %o. Skipping entry."), entryId);
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.push(entry);
|
||||
const entry = channelTree.findEntryId(event.treeEntryId);
|
||||
if(!entry) {
|
||||
logWarn(LogCategory.CHANNEL, tr("Tried to select an invalid channel tree entry with id %o"), event.treeEntryId);
|
||||
return;
|
||||
}
|
||||
|
||||
channelTree.selection.select(entries, event.mode);
|
||||
});
|
||||
|
||||
events.on("action_select_auto", event => {
|
||||
if(event.direction === "next") {
|
||||
channelTree.selection.selectNextTreeEntry();
|
||||
} else if(event.direction === "previous") {
|
||||
channelTree.selection.selectPreviousTreeEntry();
|
||||
}
|
||||
channelTree.setSelectedEntry(entry);
|
||||
});
|
||||
|
||||
events.on("action_show_context_menu", event => {
|
||||
if(event.treeEntryId === 0) {
|
||||
const entries = event.treeEntryIds.map(entryId => {
|
||||
const entry = channelTree.findEntryId(entryId);
|
||||
if(!entry) {
|
||||
logWarn(LogCategory.CHANNEL, tr("Tried to open a context menu for an invalid channel tree entry with id %o"), entryId);
|
||||
}
|
||||
return entry;
|
||||
}).filter(entry => !!entry);
|
||||
|
||||
if(entries.length === 0) {
|
||||
channelTree.showContextMenu(event.pageX, event.pageY);
|
||||
return;
|
||||
} else if(entries.length === 1) {
|
||||
entries[0].showContextMenu(event.pageX, event.pageY);
|
||||
} else {
|
||||
channelTree.showMultiSelectContextMenu(entries, event.pageX, event.pageY);
|
||||
}
|
||||
const entry = channelTree.findEntryId(event.treeEntryId);
|
||||
if(!entry) {
|
||||
logWarn(LogCategory.CHANNEL, tr("Tried to open a context menu for an invalid channel tree entry with id %o"), event.treeEntryId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (channelTree.selection.isMultiSelect() && entry.isSelected()) {
|
||||
channelTree.open_multiselect_context_menu(channelTree.selection.selectedEntries, event.pageX, event.pageY);
|
||||
return;
|
||||
}
|
||||
|
||||
channelTree.events.fire("action_select_entries", {
|
||||
entries: [entry],
|
||||
mode: "exclusive"
|
||||
});
|
||||
entry.showContextMenu(event.pageX, event.pageY);
|
||||
});
|
||||
|
||||
events.on("action_channel_join", event => {
|
||||
if(!event.ignoreMultiSelect && channelTree.selection.isMultiSelect()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = event.treeEntryId === "selected" ? channelTree.selection.selectedEntries[0] : channelTree.findEntryId(event.treeEntryId);
|
||||
const entry = channelTree.findEntryId(event.treeEntryId);
|
||||
if(!entry || !(entry instanceof ChannelEntry)) {
|
||||
if(event.treeEntryId !== "selected") {
|
||||
logWarn(LogCategory.CHANNEL, tr("Tried to join an invalid tree entry with id %o"), event.treeEntryId);
|
||||
}
|
||||
logWarn(LogCategory.CHANNEL, tr("Tried to join an invalid tree entry with id %o"), event.treeEntryId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -737,15 +712,7 @@ export function initializeChannelTreeController(events: Registry<ChannelTreeUIEv
|
|||
return;
|
||||
}
|
||||
|
||||
if(moveSelection) {
|
||||
/* don't select entries while we're moving */
|
||||
return;
|
||||
}
|
||||
|
||||
channelTree.events.fire("action_select_entries", {
|
||||
entries: [entry],
|
||||
mode: "exclusive"
|
||||
});
|
||||
channelTree.setSelectedEntry(entry);
|
||||
spawnFileTransferModal(entry.channelId);
|
||||
});
|
||||
|
||||
|
@ -756,10 +723,6 @@ export function initializeChannelTreeController(events: Registry<ChannelTreeUIEv
|
|||
return;
|
||||
}
|
||||
|
||||
if(channelTree.selection.isMultiSelect()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry instanceof LocalClientEntry) {
|
||||
entry.openRename(events);
|
||||
} else if (entry instanceof MusicClientEntry) {
|
||||
|
@ -786,46 +749,111 @@ export function initializeChannelTreeController(events: Registry<ChannelTreeUIEv
|
|||
})
|
||||
});
|
||||
|
||||
let moveSelection: ClientEntry[];
|
||||
events.on("action_start_entry_move", event => {
|
||||
const selection = channelTree.selection.selectedEntries.slice();
|
||||
if(selection.length === 0) { return; }
|
||||
if(selection.findIndex(element => !(element instanceof ClientEntry)) !== -1) { return; }
|
||||
|
||||
moveSelection = selection as any;
|
||||
events.fire_react("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;
|
||||
events.on("action_move_clients", event => {
|
||||
const entry = channelTree.findEntryId(event.targetTreeEntry);
|
||||
if(!entry) {
|
||||
logWarn(LogCategory.CHANNEL, tr("Received client move notify with an unknown target entry id %o"), event.targetTreeEntry);
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = channelTree.findEntryId(event.treeEntryId);
|
||||
|
||||
|
||||
let targetChannel: ChannelEntry;
|
||||
if(entry instanceof ChannelEntry) {
|
||||
targetChannel = entry;
|
||||
} else if(entry instanceof ClientEntry) {
|
||||
if(entry instanceof ClientEntry) {
|
||||
targetChannel = entry.currentChannel();
|
||||
} else if(entry instanceof ChannelEntry) {
|
||||
targetChannel = entry;
|
||||
} else {
|
||||
logWarn(LogCategory.CHANNEL, tr("Received client move notify with an invalid target entry id %o"), event.targetTreeEntry);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!targetChannel) {
|
||||
logWarn(LogCategory.CHANNEL, tr("Tried to move clients to an tree entry which has no target channel. Tree entry id %o"), event.treeEntryId);
|
||||
moveSelection = undefined;
|
||||
/* should not happen often that a client hasn't a channel */
|
||||
return;
|
||||
}
|
||||
|
||||
moveSelection.filter(e => e.currentChannel() !== entry).forEach(e => {
|
||||
channelTree.client.serverConnection.send_command("clientmove", {
|
||||
clid: e.clientId(),
|
||||
cid: targetChannel.channelId
|
||||
});
|
||||
const clients = event.entries.map(entryId => {
|
||||
const entry = channelTree.findEntryId(entryId);
|
||||
if(!entry || !(entry instanceof ClientEntry)) {
|
||||
logWarn(LogCategory.CHANNEL, tr("Received client move notify with an entry id which isn't a client. Entry id: %o"), entryId);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return entry;
|
||||
}).filter(client => !!client).filter(client => client.currentChannel() !== targetChannel);
|
||||
|
||||
if(clients.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let bulks = clients.map(client => { return { clid: client.clientId() }; });
|
||||
bulks[0]["cid"] = targetChannel.channelId;
|
||||
|
||||
channelTree.client.serverConnection.send_command("clientmove", bulks);
|
||||
});
|
||||
|
||||
events.on("action_move_channels", event => {
|
||||
const targetChannel = channelTree.findEntryId(event.targetTreeEntry);
|
||||
if(!targetChannel || !(targetChannel instanceof ChannelEntry)) {
|
||||
logWarn(LogCategory.CHANNEL, tr("Received channel move notify with an unknown/invalid target entry id %o"), event.targetTreeEntry);
|
||||
return;
|
||||
}
|
||||
|
||||
let channels = event.entries.map(entryId => {
|
||||
const entry = channelTree.findEntryId(entryId);
|
||||
if(!entry || !(entry instanceof ChannelEntry)) {
|
||||
logWarn(LogCategory.CHANNEL, tr("Received channel move notify with a channel id which isn't a channel. Entry id: %o"), entryId);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return entry;
|
||||
}).filter(channel => !!channel);
|
||||
|
||||
/* remove all channel in channel channels */
|
||||
channels = channels.filter(channel => {
|
||||
while((channel = channel.parent_channel())) {
|
||||
if(channels.indexOf(channel) !== -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
moveSelection = undefined;
|
||||
channels = channels.filter(channel => channel !== targetChannel);
|
||||
|
||||
if(channels.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let parentChannelId: number, previousChannelId: number;
|
||||
if(event.mode === "before") {
|
||||
parentChannelId = targetChannel.hasParent() ? targetChannel.parent_channel().channelId : 0;
|
||||
previousChannelId = targetChannel.channel_previous ? targetChannel.channel_previous.channelId : 0;
|
||||
} else if(event.mode == "after") {
|
||||
parentChannelId = targetChannel.hasParent() ? targetChannel.parent_channel().channelId : 0;
|
||||
previousChannelId = targetChannel.channelId;
|
||||
} else if(event.mode === "child") {
|
||||
parentChannelId = targetChannel.channelId;
|
||||
previousChannelId = 0;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
let channel: ChannelEntry;
|
||||
while((channel = channels.pop_front())) {
|
||||
const success = await channelTree.client.serverConnection.send_command("channelmove", {
|
||||
cid: channel.channelId,
|
||||
cpid: parentChannelId,
|
||||
order: previousChannelId
|
||||
}).then(() => true).catch(() => false);
|
||||
|
||||
if(!success) {
|
||||
break;
|
||||
}
|
||||
|
||||
previousChannelId = channel.channelId;
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
events.on("notify_client_name_edit_failed", event => {
|
||||
|
|
|
@ -29,27 +29,23 @@ export type ServerState = { state: "disconnected" } | { state: "connecting", tar
|
|||
export interface ChannelTreeUIEvents {
|
||||
/* actions */
|
||||
action_toggle_popout: { shown: boolean },
|
||||
action_show_context_menu: { treeEntryId: number | 0, pageX: number, pageY: number },
|
||||
action_show_context_menu: { treeEntryIds: number[], pageX: number, pageY: number },
|
||||
action_start_entry_move: { start: { x: number, y: number }, current: { x: number, y: number } },
|
||||
action_set_collapsed_state: { treeEntryId: number, state: "collapsed" | "expended" },
|
||||
action_select: {
|
||||
entryIds: number[],
|
||||
mode: "auto" | "auto-add" | "exclusive" | "append" | "remove",
|
||||
ignoreClientMove: boolean
|
||||
},
|
||||
action_select_auto: { direction: "next" | "previous" },
|
||||
action_channel_join: { treeEntryId: number | "selected", ignoreMultiSelect: boolean },
|
||||
action_select: { treeEntryId: number | 0 },
|
||||
action_channel_join: { treeEntryId: number },
|
||||
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 */ }
|
||||
action_move_clients: { targetTreeEntry: number, entries: number[] },
|
||||
action_move_channels: { targetTreeEntry: number, mode: "before" | "after" | "child", entries: number[] },
|
||||
|
||||
/* queries */
|
||||
query_tree_entries: {},
|
||||
query_popout_state: {},
|
||||
query_selected_entry: {},
|
||||
|
||||
query_unread_state: { treeEntryId: number },
|
||||
query_select_state: { treeEntryId: number },
|
||||
|
||||
query_channel_info: { treeEntryId: number },
|
||||
query_channel_icon: { treeEntryId: number },
|
||||
|
@ -65,6 +61,7 @@ export interface ChannelTreeUIEvents {
|
|||
/* notifies */
|
||||
notify_tree_entries: { entries: ChannelTreeEntry[] },
|
||||
notify_popout_state: { shown: boolean, showButton: boolean },
|
||||
notify_selected_entry: { treeEntryId: number | 0 },
|
||||
|
||||
notify_channel_info: { treeEntryId: number, info: ChannelEntryInfo },
|
||||
notify_channel_icon: { treeEntryId: number, icon: ClientIcon },
|
||||
|
@ -80,10 +77,16 @@ export interface ChannelTreeUIEvents {
|
|||
notify_server_state: { treeEntryId: number, state: ServerState },
|
||||
|
||||
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: {}
|
||||
}
|
||||
}
|
||||
|
||||
export type ChannelTreeDragData = {
|
||||
version: 1,
|
||||
handlerId: string,
|
||||
type: string,
|
||||
|
||||
entryIds: number[],
|
||||
entryTypes: ("server" | "channel" | "client")[]
|
||||
};
|
|
@ -0,0 +1,188 @@
|
|||
import {RDPChannel, RDPChannelTree, RDPClient, RDPEntry, RDPServer} from "./RendererDataProvider";
|
||||
import * as loader from "tc-loader";
|
||||
import {Stage} from "tc-loader";
|
||||
import {
|
||||
ClientIcon,
|
||||
spriteEntries as kClientSpriteEntries,
|
||||
spriteHeight as kClientSpriteHeight,
|
||||
spriteUrl as kClientSpriteUrl,
|
||||
spriteWidth as kClientSpriteWidth,
|
||||
} from "svg-sprites/client-icons";
|
||||
import {LogCategory, logDebug} from "tc-shared/log";
|
||||
import {ChannelTreeDragData} from "tc-shared/ui/tree/Definitions";
|
||||
|
||||
let spriteImage: HTMLImageElement;
|
||||
|
||||
async function initializeIconSpreedSheet() {
|
||||
spriteImage = new Image(kClientSpriteWidth, kClientSpriteHeight);
|
||||
spriteImage.src = loader.config.baseUrl + kClientSpriteUrl;
|
||||
await new Promise((resolve, reject) => {
|
||||
spriteImage.onload = resolve;
|
||||
spriteImage.onerror = () => reject("failed to load client icon sprite");
|
||||
});
|
||||
}
|
||||
|
||||
function paintClientIcon(context: CanvasRenderingContext2D, icon: ClientIcon, offsetX: number, offsetY: number, width: number, height: number) {
|
||||
const sprite = kClientSpriteEntries.find(e => e.className === icon);
|
||||
if(!sprite) return undefined;
|
||||
|
||||
context.drawImage(spriteImage, sprite.xOffset, sprite.yOffset, sprite.width, sprite.height, offsetX, offsetY, width, height);
|
||||
}
|
||||
|
||||
export function generateDragElement(entries: RDPEntry[]) : HTMLElement {
|
||||
const totalHeight = entries.length * 18 + 2; /* the two extra for "low" letters like "gyj" etc. */
|
||||
const totalWidth = 250;
|
||||
|
||||
let offsetY = 0;
|
||||
let offsetX = 20;
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.height = totalHeight;
|
||||
canvas.width = totalWidth;
|
||||
|
||||
/* TODO: With font size? */
|
||||
{
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.textAlign = "left";
|
||||
ctx.textBaseline = "bottom";
|
||||
ctx.font = "700 16px Roboto, Helvetica, Arial, sans-serif";
|
||||
|
||||
for(const entry of entries) {
|
||||
let name: string;
|
||||
let icon: ClientIcon;
|
||||
|
||||
if(entry instanceof RDPClient) {
|
||||
name = entry.name.name;
|
||||
icon = entry.status;
|
||||
} else if(entry instanceof RDPChannel) {
|
||||
name = entry.info?.name;
|
||||
icon = entry.icon;
|
||||
} else if(entry instanceof RDPServer) {
|
||||
icon = ClientIcon.ServerGreen;
|
||||
|
||||
switch (entry.state.state) {
|
||||
case "connected":
|
||||
name = entry.state.name;
|
||||
break;
|
||||
|
||||
case "disconnected":
|
||||
name = tr("Not connected");
|
||||
break;
|
||||
|
||||
case "connecting":
|
||||
name = tr("Connecting");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
ctx.strokeStyle = "red";
|
||||
ctx.moveTo(offsetX, offsetY);
|
||||
ctx.lineTo(offsetX + 1000, offsetY);
|
||||
ctx.stroke();
|
||||
*/
|
||||
|
||||
ctx.fillStyle = "black";
|
||||
paintClientIcon(ctx, icon, offsetX + 1, offsetY + 1, 16, 16);
|
||||
ctx.fillText(name, offsetX + 20, offsetY + 19);
|
||||
|
||||
offsetY += 18;
|
||||
|
||||
/*
|
||||
ctx.strokeStyle = "blue";
|
||||
ctx.moveTo(offsetX, offsetY);
|
||||
ctx.lineTo(offsetX + 1000, offsetY);
|
||||
ctx.stroke();
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
canvas.style.position = "absolute";
|
||||
canvas.style.left = "-100000000px";
|
||||
canvas.style.top = (Math.random() * 1000000).toFixed(0) + "px";
|
||||
document.body.appendChild(canvas);
|
||||
|
||||
setTimeout(() => {
|
||||
canvas.remove();
|
||||
}, 50);
|
||||
|
||||
return canvas as any;
|
||||
}
|
||||
|
||||
const kDragDataType = "application/x-teaspeak-channel-move";
|
||||
const kDragHandlerPrefix = "application/x-teaspeak-handler-";
|
||||
const kDragTypePrefix = "application/x-teaspeak-type-";
|
||||
|
||||
export function setupDragData(transfer: DataTransfer, tree: RDPChannelTree, entries: RDPEntry[], type: string) {
|
||||
let data: ChannelTreeDragData = {
|
||||
version: 1,
|
||||
handlerId: tree.handlerId,
|
||||
entryIds: entries.map(e => e.entryId),
|
||||
entryTypes: entries.map(entry => {
|
||||
if(entry instanceof RDPServer) {
|
||||
return "server";
|
||||
} else if(entry instanceof RDPClient) {
|
||||
return "client";
|
||||
} else {
|
||||
return "channel";
|
||||
}
|
||||
}),
|
||||
type: type
|
||||
};
|
||||
|
||||
transfer.effectAllowed = "all"
|
||||
transfer.dropEffect = "move";
|
||||
transfer.setData(kDragHandlerPrefix + tree.handlerId, "");
|
||||
transfer.setData(kDragTypePrefix + type, "");
|
||||
transfer.setData(kDragDataType, JSON.stringify(data));
|
||||
|
||||
{
|
||||
let texts = [];
|
||||
for(const entry of entries) {
|
||||
if(entry instanceof RDPClient) {
|
||||
texts.push(entry.name?.name);
|
||||
} else if(entry instanceof RDPChannel) {
|
||||
texts.push(entry.info?.name);
|
||||
} else if(entry instanceof RDPServer) {
|
||||
texts.push(entry.state.state === "connected" ? entry.state.name : undefined);
|
||||
}
|
||||
}
|
||||
transfer.setData("text/plain", texts.filter(e => !!e).join(", "));
|
||||
}
|
||||
/* TODO: Other things as well! */
|
||||
}
|
||||
|
||||
export function parseDragData(transfer: DataTransfer) : ChannelTreeDragData | undefined {
|
||||
const rawData = transfer.getData(kDragDataType);
|
||||
if(!rawData) { return undefined; }
|
||||
|
||||
try {
|
||||
const data = JSON.parse(rawData) as ChannelTreeDragData;
|
||||
if(data.version !== 1) { throw tra("invalid data version {}", data.version); }
|
||||
return data;
|
||||
} catch (error) {
|
||||
logDebug(LogCategory.GENERAL, tr("Drag data parsing failed: %o"), error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getDragInfo(transfer: DataTransfer) : { handlerId: string, type: string } | undefined {
|
||||
const keys = [...transfer.items].filter(e => e.kind === "string").map(e => e.type);
|
||||
const handlerId = keys.find(e => e.startsWith(kDragHandlerPrefix))?.substr(kDragHandlerPrefix.length);
|
||||
const type = keys.find(e => e.startsWith(kDragTypePrefix))?.substr(kDragTypePrefix.length);
|
||||
|
||||
if(!handlerId || !type) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
handlerId: handlerId,
|
||||
type: type
|
||||
}
|
||||
}
|
||||
|
||||
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "Icon sprite loader",
|
||||
function: async () => await initializeIconSpreedSheet(),
|
||||
priority: 10
|
||||
});
|
|
@ -2,7 +2,7 @@ import {Registry} from "tc-shared/events";
|
|||
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
|
||||
import * as React from "react";
|
||||
import {ChannelTreeView, PopoutButton} from "tc-shared/ui/tree/RendererView";
|
||||
import {RDPChannelTree} from "./RendererDataProvider";
|
||||
import {RDPChannel, RDPChannelTree} from "./RendererDataProvider";
|
||||
import {useEffect, useRef} from "react";
|
||||
|
||||
const viewStyle = require("./View.scss");
|
||||
|
@ -34,13 +34,17 @@ const ContainerView = (props: { tree: RDPChannelTree, events: Registry<ChannelTr
|
|||
|
||||
if(event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
props.events.fire("action_select_auto", { direction: "previous" });
|
||||
props.tree.selection.selectNext(true, "up");
|
||||
} else if(event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
props.events.fire("action_select_auto", { direction: "next" });
|
||||
props.tree.selection.selectNext(true, "down");
|
||||
} else if(event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
props.events.fire("action_channel_join", { treeEntryId: "selected", ignoreMultiSelect: false });
|
||||
|
||||
const selectedEntries = props.tree.selection.selectedEntries;
|
||||
if(selectedEntries.length !== 1) { return; }
|
||||
if(!(selectedEntries[0] instanceof RDPChannel)) { return; }
|
||||
props.events.fire("action_channel_join", { treeEntryId: selectedEntries[0].entryId });
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -123,29 +123,31 @@ export class RendererChannel extends React.Component<{ channel: RDPChannel }, {}
|
|||
);
|
||||
}
|
||||
|
||||
let dragClass;
|
||||
if(channel.dragHint !== "none") {
|
||||
dragClass = channelStyle["drag-" + channel.dragHint];
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={viewStyle.treeEntry + " " + channelStyle.channelEntry + " " + (channel.selected ? viewStyle.selected : "")}
|
||||
ref={this.props.channel.refChannelContainer}
|
||||
className={viewStyle.treeEntry + " " + channelStyle.channelEntry + " " + (channel.selected ? viewStyle.selected : "") + " " + dragClass}
|
||||
style={{ top: channel.offsetTop }}
|
||||
onMouseUp={event => {
|
||||
if (event.button !== 0) {
|
||||
return; /* only left mouse clicks */
|
||||
}
|
||||
|
||||
events.fire("action_select", {
|
||||
entryIds: [ entryId ],
|
||||
mode: "auto",
|
||||
ignoreClientMove: false
|
||||
});
|
||||
this.props.channel.select("auto");
|
||||
}}
|
||||
onDoubleClick={() => events.fire("action_channel_join", { ignoreMultiSelect: false, treeEntryId: entryId })}
|
||||
onDoubleClick={() => events.fire("action_channel_join", { treeEntryId: entryId })}
|
||||
onContextMenu={event => {
|
||||
if (settings.static(Settings.KEY_DISABLE_CONTEXT_MENU)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
events.fire("action_show_context_menu", { treeEntryId: entryId, pageX: event.pageX, pageY: event.pageY });
|
||||
this.props.channel.handleUiContextMenu(event.pageX, event.pageY);
|
||||
}}
|
||||
onMouseDown={event => {
|
||||
if (event.buttons !== 4) {
|
||||
|
@ -155,8 +157,12 @@ export class RendererChannel extends React.Component<{ channel: RDPChannel }, {}
|
|||
event.preventDefault();
|
||||
events.fire("action_channel_open_file_browser", { treeEntryId: entryId });
|
||||
}}
|
||||
draggable={true}
|
||||
onDragStart={event => this.props.channel.handleUiDragStart(event.nativeEvent)}
|
||||
onDragOver={event => this.props.channel.handleUiDragOver(event.nativeEvent)}
|
||||
onDrop={event => this.props.channel.handleUiDrop(event.nativeEvent)}
|
||||
>
|
||||
<div className={viewStyle.leftPadding} style={{ paddingLeft: channel.offsetLeft + "em" }} />
|
||||
<div className={viewStyle.leftPadding + " " + channelStyle.leftPadding} style={{ paddingLeft: channel.offsetLeft + "em" }} />
|
||||
<UnreadMarkerRenderer entry={this.props.channel} ref={this.props.channel.refUnread} />
|
||||
{collapsedIndicator}
|
||||
{channelIcon}
|
||||
|
|
|
@ -183,27 +183,27 @@ export class RendererClient extends React.Component<{ client: RDPClient }, {}> {
|
|||
}
|
||||
|
||||
event.preventDefault();
|
||||
events.fire("action_show_context_menu", { treeEntryId: client.entryId, pageX: event.pageX, pageY: event.pageY });
|
||||
this.props.client.handleUiContextMenu(event.pageX, event.pageY);
|
||||
}}
|
||||
onMouseUp={event => {
|
||||
if (event.button !== 0) {
|
||||
return; /* only left mouse clicks */
|
||||
}
|
||||
|
||||
events.fire("action_select", {
|
||||
entryIds: [ client.entryId ],
|
||||
mode: "auto",
|
||||
ignoreClientMove: false
|
||||
});
|
||||
this.props.client.select("auto");
|
||||
}}
|
||||
onDoubleClick={() => events.fire("action_client_double_click", { treeEntryId: client.entryId })}
|
||||
onDoubleClick={() => this.props.client.handleUiDoubleClicked()}
|
||||
draggable={!client.rename}
|
||||
onDragStart={event => this.props.client.handleUiDragStart(event.nativeEvent)}
|
||||
onDragOver={event => this.props.client.handleUiDragOver(event.nativeEvent)}
|
||||
onDrop={event => this.props.client.handleUiDrop(event.nativeEvent)}
|
||||
>
|
||||
<div className={viewStyle.leftPadding} style={{ paddingLeft: client.offsetLeft + "em" }} />
|
||||
<UnreadMarkerRenderer entry={client} ref={client.refUnread} />
|
||||
<ClientStatus client={client} ref={client.refStatus} />
|
||||
{...(client.rename ? [
|
||||
<ClientNameEdit initialName={client.renameDefault} editFinished={value => {
|
||||
events.fire_react("action_client_name_submit", { treeEntryId: client.entryId, name: value });
|
||||
events.fire("action_client_name_submit", { treeEntryId: client.entryId, name: value });
|
||||
}} key={"rename"} />
|
||||
] : [
|
||||
<ClientName client={client} ref={client.refName} key={"name"} />,
|
||||
|
|
|
@ -4,7 +4,9 @@ import {
|
|||
ChannelIcons,
|
||||
ChannelTreeUIEvents,
|
||||
ClientIcons,
|
||||
ClientNameInfo, ClientTalkIconState, ServerState
|
||||
ClientNameInfo,
|
||||
ClientTalkIconState,
|
||||
ServerState
|
||||
} from "tc-shared/ui/tree/Definitions";
|
||||
import {ChannelTreeView, PopoutButton} from "tc-shared/ui/tree/RendererView";
|
||||
import * as React from "react";
|
||||
|
@ -20,7 +22,8 @@ import {
|
|||
RendererClient
|
||||
} from "tc-shared/ui/tree/RendererClient";
|
||||
import {ServerRenderer} from "tc-shared/ui/tree/RendererServer";
|
||||
import {RendererMove} from "./RendererMove";
|
||||
import {generateDragElement, getDragInfo, parseDragData, setupDragData} from "tc-shared/ui/tree/DragHelper";
|
||||
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
||||
|
||||
function isEquivalent(a, b) {
|
||||
const typeA = typeof a;
|
||||
|
@ -61,16 +64,192 @@ function isEquivalent(a, b) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* auto := Select/unselect/add/remove depending on the selected state & shift key state
|
||||
* exclusive := Only selected these entries
|
||||
* append := Append these entries to the current selection
|
||||
* remove := Remove these entries from the current selection
|
||||
*/
|
||||
export type RDPTreeSelectType = "auto" | "auto-add" | "exclusive" | "append" | "remove";
|
||||
export class RDPTreeSelection {
|
||||
readonly handle: RDPChannelTree;
|
||||
selectedEntries: RDPEntry[] = [];
|
||||
|
||||
private readonly documentKeyListener;
|
||||
private readonly documentBlurListener;
|
||||
private shiftKeyPressed = false;
|
||||
|
||||
constructor(handle: RDPChannelTree) {
|
||||
this.handle = handle;
|
||||
|
||||
this.documentKeyListener = event => this.shiftKeyPressed = event.shiftKey;
|
||||
this.documentBlurListener = () => this.shiftKeyPressed = false;
|
||||
|
||||
document.addEventListener("keydown", this.documentKeyListener);
|
||||
document.addEventListener("keyup", this.documentKeyListener);
|
||||
document.addEventListener("focusout", this.documentBlurListener);
|
||||
document.addEventListener("mouseout", this.documentBlurListener);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.clearSelection();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
document.removeEventListener("keydown", this.documentKeyListener);
|
||||
document.removeEventListener("keyup", this.documentKeyListener);
|
||||
document.removeEventListener("focusout", this.documentBlurListener);
|
||||
document.removeEventListener("mouseout", this.documentBlurListener);
|
||||
this.selectedEntries.splice(0, this.selectedEntries.length);
|
||||
}
|
||||
|
||||
isMultiSelect() {
|
||||
return this.selectedEntries.length > 1;
|
||||
}
|
||||
|
||||
isAnythingSelected() {
|
||||
return this.selectedEntries.length > 0;
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this.select([], "exclusive", false);
|
||||
}
|
||||
|
||||
select(entries: RDPEntry[], mode: RDPTreeSelectType, selectMaintree: boolean) {
|
||||
entries = entries.filter(entry => !!entry);
|
||||
|
||||
let deletedEntries: RDPEntry[] = [];
|
||||
let newEntries: RDPEntry[] = [];
|
||||
|
||||
if(mode === "exclusive") {
|
||||
deletedEntries = this.selectedEntries.slice();
|
||||
newEntries = entries;
|
||||
} else if(mode === "append") {
|
||||
newEntries = entries;
|
||||
} else if(mode === "remove") {
|
||||
deletedEntries = entries;
|
||||
} else if(mode === "auto" || mode === "auto-add") {
|
||||
if(this.shiftKeyPressed) {
|
||||
for(const entry of entries) {
|
||||
const index = this.selectedEntries.findIndex(e => e === entry);
|
||||
if(index === -1) {
|
||||
newEntries.push(entry);
|
||||
} else if(mode === "auto") {
|
||||
deletedEntries.push(entry);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
deletedEntries = this.selectedEntries.slice();
|
||||
if(entries.length !== 0) {
|
||||
const entry = entries[entries.length - 1];
|
||||
if(!deletedEntries.remove(entry)) {
|
||||
newEntries.push(entry); /* entry wans't selected yet */
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn("Received entry select event with unknown mode: %s", mode);
|
||||
}
|
||||
|
||||
newEntries.forEach(entry => deletedEntries.remove(entry));
|
||||
newEntries = newEntries.filter(entry => {
|
||||
if(this.selectedEntries.indexOf(entry) === -1) {
|
||||
this.selectedEntries.push(entry);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
deletedEntries = deletedEntries.filter(entry => this.selectedEntries.remove(entry));
|
||||
|
||||
deletedEntries.forEach(entry => entry.setSelected(false));
|
||||
newEntries.forEach(entry => entry.setSelected(true));
|
||||
|
||||
/* it's important to keep it sorted from the top to the bottom (example would be the channel move) */
|
||||
if(deletedEntries.length > 0 || newEntries.length > 0) {
|
||||
const treeEntries = this.handle.getTreeEntries();
|
||||
|
||||
const lookupMap = {};
|
||||
this.selectedEntries.forEach(entry => lookupMap[entry.entryId] = treeEntries.indexOf(entry));
|
||||
this.selectedEntries.sort((a, b) => lookupMap[a.entryId] - lookupMap[b.entryId]);
|
||||
}
|
||||
|
||||
if(this.selectedEntries.length === 1 && selectMaintree) {
|
||||
this.handle.events.fire("action_select", { treeEntryId: this.selectedEntries[0].entryId });
|
||||
}
|
||||
}
|
||||
|
||||
public selectNext(selectClients: boolean, direction: "up" | "down") {
|
||||
const entries = this.handle.getTreeEntries();
|
||||
const selectedEntriesIndex = this.selectedEntries.map(e => entries.indexOf(e)).filter(e => e !== -1);
|
||||
|
||||
let index;
|
||||
if(direction === "up") {
|
||||
index = selectedEntriesIndex.reduce((previousValue, currentValue) => Math.min(previousValue, currentValue), entries.length);
|
||||
if(index === entries.length) {
|
||||
index = entries.length - 1;
|
||||
}
|
||||
} else {
|
||||
index = selectedEntriesIndex.reduce((previousValue, currentValue) => Math.max(previousValue, currentValue), -1);
|
||||
if(index === -1) {
|
||||
index = entries.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if(index === -1) {
|
||||
/* tree contains no entries */
|
||||
return;
|
||||
}
|
||||
|
||||
if(!this.doSelectNext(entries[index], selectClients, direction)) {
|
||||
/* There is no next entry. Select the last one. */
|
||||
if(this.isMultiSelect()) {
|
||||
this.select([ entries[index] ], "exclusive", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private doSelectNext(current: RDPEntry, selectClients: boolean, direction: "up" | "down") : boolean {
|
||||
const directionModifier = direction === "down" ? 1 : -1;
|
||||
|
||||
const entries = this.handle.getTreeEntries();
|
||||
let index = entries.indexOf(current);
|
||||
if(index === -1) { return false; }
|
||||
index += directionModifier;
|
||||
|
||||
if(selectClients) {
|
||||
if(index >= entries.length || index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.select([entries[index]], "exclusive", true);
|
||||
return true;
|
||||
} else {
|
||||
while(index >= 0 && index < entries.length && entries[index] instanceof RDPClient) {
|
||||
index += directionModifier;
|
||||
}
|
||||
|
||||
if(index === entries.length || index <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.select([entries[index]], "exclusive", true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RDPChannelTree {
|
||||
readonly events: Registry<ChannelTreeUIEvents>;
|
||||
readonly handlerId: string;
|
||||
|
||||
private registeredEventHandlers = [];
|
||||
|
||||
readonly refMove = React.createRef<RendererMove>();
|
||||
readonly refTree = React.createRef<ChannelTreeView>();
|
||||
readonly refPopoutButton = React.createRef<PopoutButton>();
|
||||
|
||||
readonly selection: RDPTreeSelection;
|
||||
|
||||
popoutShown: boolean = false;
|
||||
popoutButtonShown: boolean = false;
|
||||
|
||||
|
@ -78,9 +257,12 @@ export class RDPChannelTree {
|
|||
private orderedTree: RDPEntry[] = [];
|
||||
private treeEntries: {[key: number]: RDPEntry} = {};
|
||||
|
||||
private dragOverChannelEntry: RDPChannel;
|
||||
|
||||
constructor(events: Registry<ChannelTreeUIEvents>, handlerId: string) {
|
||||
this.events = events;
|
||||
this.handlerId = handlerId;
|
||||
this.selection = new RDPTreeSelection(this);
|
||||
}
|
||||
|
||||
initialize() {
|
||||
|
@ -98,17 +280,6 @@ export class RDPChannelTree {
|
|||
entry.handleUnreadUpdate(event.unread);
|
||||
}));
|
||||
|
||||
events.push(this.events.on("notify_select_state", event => {
|
||||
const entry = this.treeEntries[event.treeEntryId];
|
||||
if(!entry) {
|
||||
logError(LogCategory.CHANNEL, tr("Received select notify for invalid tree entry %o."), event.treeEntryId);
|
||||
return;
|
||||
}
|
||||
|
||||
entry.handleSelectUpdate(event.selected);
|
||||
}));
|
||||
|
||||
|
||||
events.push(this.events.on("notify_channel_info", event => {
|
||||
const entry = this.treeEntries[event.treeEntryId];
|
||||
if(!entry || !(entry instanceof RDPChannel)) {
|
||||
|
@ -201,8 +372,17 @@ export class RDPChannelTree {
|
|||
entry.handleStateUpdate(event.state);
|
||||
}));
|
||||
|
||||
events.push(this.events.on("notify_selected_entry", event => {
|
||||
const entry = this.getTreeEntries().find(entry => entry.entryId === event.treeEntryId);
|
||||
this.selection.select(entry ? [entry] : [], "exclusive", false);
|
||||
if(entry) {
|
||||
this.refTree.current?.scrollEntryInView(entry.entryId);
|
||||
}
|
||||
}));
|
||||
|
||||
this.events.fire("query_tree_entries");
|
||||
this.events.fire("query_popout_state");
|
||||
this.events.fire("query_selected_entry");
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
@ -215,6 +395,129 @@ export class RDPChannelTree {
|
|||
return this.orderedTree;
|
||||
}
|
||||
|
||||
handleDragStart(event: DragEvent) {
|
||||
const entries = this.selection.selectedEntries;
|
||||
if(entries.length === 0) {
|
||||
/* should never happen */
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
let dragType;
|
||||
if(entries.findIndex(e => !(e instanceof RDPClient)) === -1) {
|
||||
/* clients only => move */
|
||||
event.dataTransfer.effectAllowed = "move"; /* prohibit copying */
|
||||
dragType = "client";
|
||||
} else if(entries.findIndex(e => !(e instanceof RDPServer)) === -1) {
|
||||
/* server only => doing nothing right now */
|
||||
event.preventDefault();
|
||||
return;
|
||||
} else if(entries.findIndex(e => !(e instanceof RDPChannel)) === -1) {
|
||||
/* channels only => move */
|
||||
event.dataTransfer.effectAllowed = "all";
|
||||
dragType = "channel";
|
||||
} else {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
event.dataTransfer.setDragImage(generateDragElement(entries), 0, 6);
|
||||
setupDragData(event.dataTransfer, this, entries, dragType);
|
||||
}
|
||||
|
||||
|
||||
handleUiDragOver(event: DragEvent, target: RDPEntry) {
|
||||
if(this.dragOverChannelEntry !== target) {
|
||||
this.dragOverChannelEntry?.setDragHint("none");
|
||||
this.dragOverChannelEntry = undefined;
|
||||
}
|
||||
|
||||
const info = getDragInfo(event.dataTransfer);
|
||||
if(!info) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.dataTransfer.dropEffect = info.handlerId === this.handlerId ? "move" : "copy";
|
||||
if(info.type === "client") {
|
||||
if(target instanceof RDPServer) {
|
||||
/* can't move a client into a server */
|
||||
return;
|
||||
}
|
||||
|
||||
/* clients can be dropped anywhere (if they're getting dropped on another client we'll use use his channel */
|
||||
event.preventDefault();
|
||||
return;
|
||||
} else if(info.type === "channel") {
|
||||
if(!(target instanceof RDPChannel) || !target.refChannelContainer.current) {
|
||||
/* channel could only be moved into channels */
|
||||
return;
|
||||
}
|
||||
|
||||
const containerPosition = target.refChannelContainer.current.getBoundingClientRect();
|
||||
const offsetY = (event.pageY - containerPosition.y) / containerPosition.height;
|
||||
|
||||
if(offsetY <= .25) {
|
||||
target.setDragHint("top");
|
||||
} else if(offsetY <= .75) {
|
||||
target.setDragHint("contain");
|
||||
} else {
|
||||
target.setDragHint("bottom");
|
||||
}
|
||||
|
||||
this.dragOverChannelEntry = target;
|
||||
event.preventDefault();
|
||||
} else {
|
||||
/* unknown => not supported */
|
||||
}
|
||||
}
|
||||
|
||||
handleUiDrop(event: DragEvent, target: RDPEntry) {
|
||||
let currentDragHint: RDPChannelDragHint;
|
||||
if(this.dragOverChannelEntry) {
|
||||
currentDragHint = this.dragOverChannelEntry?.dragHint;
|
||||
this.dragOverChannelEntry?.setDragHint("none");
|
||||
this.dragOverChannelEntry = undefined;
|
||||
}
|
||||
|
||||
const data = parseDragData(event.dataTransfer);
|
||||
if(!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
if(data.type === "client") {
|
||||
if(data.handlerId !== this.handlerId) {
|
||||
createErrorModal(tr("Action not possible"), tr("You can't move clients between different server connections.")).open();
|
||||
return;
|
||||
}
|
||||
|
||||
this.events.fire("action_move_clients", {
|
||||
entries: data.entryIds,
|
||||
targetTreeEntry: target.entryId
|
||||
});
|
||||
} else if(data.type === "channel") {
|
||||
if(!(target instanceof RDPChannel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error("hint: %o", currentDragHint);
|
||||
if(!currentDragHint || currentDragHint === "none") {
|
||||
return;
|
||||
}
|
||||
|
||||
if(data.entryIds.indexOf(target.entryId) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.events.fire("action_move_channels", {
|
||||
targetTreeEntry: target.entryId,
|
||||
mode: currentDragHint === "contain" ? "child" : currentDragHint === "top" ? "before" : "after",
|
||||
entries: data.entryIds
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler<ChannelTreeUIEvents>("notify_tree_entries")
|
||||
private handleNotifyTreeEntries(event: ChannelTreeUIEvents["notify_tree_entries"]) {
|
||||
const oldEntryInstances = this.treeEntries;
|
||||
|
@ -253,9 +556,15 @@ export class RDPChannelTree {
|
|||
return result;
|
||||
}).filter(e => !!e);
|
||||
|
||||
Object.keys(oldEntryInstances).map(key => oldEntryInstances[key]).forEach(entry => {
|
||||
entry.destroy();
|
||||
});
|
||||
const removedEntries = Object.keys(oldEntryInstances).map(key => oldEntryInstances[key]);
|
||||
if(removedEntries.indexOf(this.dragOverChannelEntry) !== -1) {
|
||||
this.dragOverChannelEntry = undefined;
|
||||
}
|
||||
|
||||
if(removedEntries.length > 0) {
|
||||
this.selection.select(removedEntries, "remove", false);
|
||||
removedEntries.forEach(entry => entry.destroy());
|
||||
}
|
||||
|
||||
this.refTree.current?.setState({
|
||||
tree: this.orderedTree.slice(),
|
||||
|
@ -264,16 +573,6 @@ export class RDPChannelTree {
|
|||
}
|
||||
|
||||
|
||||
@EventHandler<ChannelTreeUIEvents>("notify_entry_move")
|
||||
private handleNotifyEntryMove(event: ChannelTreeUIEvents["notify_entry_move"]) {
|
||||
if(!this.refMove.current) {
|
||||
this.events.fire_react("action_move_entries", { treeEntryId: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
this.refMove.current.enableEntryMove(event.entries, event.begin, event.current);
|
||||
}
|
||||
|
||||
@EventHandler<ChannelTreeUIEvents>("notify_popout_state")
|
||||
private handleNotifyPopoutState(event: ChannelTreeUIEvents["notify_popout_state"]) {
|
||||
this.popoutShown = event.shown;
|
||||
|
@ -322,7 +621,6 @@ export abstract class RDPEntry {
|
|||
const events = this.getEvents();
|
||||
|
||||
events.fire("query_unread_state", { treeEntryId: this.entryId });
|
||||
events.fire("query_select_state", { treeEntryId: this.entryId });
|
||||
}
|
||||
|
||||
handleUnreadUpdate(value: boolean) {
|
||||
|
@ -332,7 +630,7 @@ export abstract class RDPEntry {
|
|||
this.refUnread.current?.forceUpdate();
|
||||
}
|
||||
|
||||
handleSelectUpdate(value: boolean) {
|
||||
setSelected(value: boolean) {
|
||||
if(this.selected === value) { return; }
|
||||
|
||||
this.selected = value;
|
||||
|
@ -353,16 +651,52 @@ export abstract class RDPEntry {
|
|||
return this.renderedInstance = this.doRender();
|
||||
}
|
||||
|
||||
select(mode: RDPTreeSelectType) {
|
||||
this.handle.selection.select([ this ], mode, true);
|
||||
}
|
||||
|
||||
handleUiDoubleClicked() {
|
||||
this.select("exclusive");
|
||||
this.getEvents().fire("action_client_double_click", { treeEntryId: this.entryId });
|
||||
}
|
||||
|
||||
handleUiContextMenu(pageX: number, pageY: number) {
|
||||
this.select("auto-add");
|
||||
this.getEvents().fire("action_show_context_menu", {
|
||||
pageX: pageX,
|
||||
pageY: pageY,
|
||||
treeEntryIds: this.handle.selection.selectedEntries.map(entry => entry.entryId)
|
||||
});
|
||||
}
|
||||
|
||||
handleUiDragStart(event: DragEvent) {
|
||||
if(!this.selected) {
|
||||
this.handle.selection.select([ this ], "exclusive", true);
|
||||
}
|
||||
|
||||
this.handle.handleDragStart(event);
|
||||
}
|
||||
|
||||
handleUiDragOver(event: DragEvent) {
|
||||
this.handle.handleUiDragOver(event, this);
|
||||
}
|
||||
|
||||
handleUiDrop(event: DragEvent) {
|
||||
this.handle.handleUiDrop(event, this);
|
||||
}
|
||||
|
||||
protected abstract doRender() : React.ReactElement;
|
||||
|
||||
protected abstract renderSelectStateUpdate();
|
||||
protected abstract renderPositionUpdate();
|
||||
}
|
||||
|
||||
export type RDPChannelDragHint = "none" | "top" | "bottom" | "contain";
|
||||
export class RDPChannel extends RDPEntry {
|
||||
readonly refIcon = React.createRef<ChannelIconClass>();
|
||||
readonly refIcons = React.createRef<ChannelIconsRenderer>();
|
||||
readonly refChannel = React.createRef<RendererChannel>();
|
||||
readonly refChannelContainer = React.createRef<HTMLDivElement>();
|
||||
|
||||
/* if uninitialized, undefined */
|
||||
info: ChannelEntryInfo;
|
||||
|
@ -373,8 +707,12 @@ export class RDPChannel extends RDPEntry {
|
|||
/* if uninitialized, undefined */
|
||||
icons: ChannelIcons;
|
||||
|
||||
dragHint: "none" | "top" | "bottom" | "contain";
|
||||
|
||||
constructor(handle: RDPChannelTree, entryId: number) {
|
||||
super(handle, entryId);
|
||||
|
||||
this.dragHint = "none";
|
||||
}
|
||||
|
||||
doRender(): React.ReactElement {
|
||||
|
@ -418,6 +756,13 @@ export class RDPChannel extends RDPEntry {
|
|||
this.info = newInfo;
|
||||
this.refChannel.current?.forceUpdate();
|
||||
}
|
||||
|
||||
setDragHint(hint: RDPChannelDragHint) {
|
||||
if(this.dragHint === hint) { return; }
|
||||
|
||||
this.dragHint = hint;
|
||||
this.refChannel.current?.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
export class RDPClient extends RDPEntry {
|
||||
|
|
|
@ -1,116 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -13,7 +13,6 @@ export class ServerRenderer extends React.Component<{ server: RDPServer }, {}> {
|
|||
render() {
|
||||
const server = this.props.server;
|
||||
const selected = this.props.server.selected;
|
||||
const events = server.getEvents();
|
||||
|
||||
let name, icon;
|
||||
switch (server.state?.state) {
|
||||
|
@ -39,16 +38,12 @@ export class ServerRenderer extends React.Component<{ server: RDPServer }, {}> {
|
|||
<div
|
||||
className={serverStyle.serverEntry + " " + viewStyle.treeEntry + " " + (selected ? viewStyle.selected : "")}
|
||||
style={{ top: server.offsetTop }}
|
||||
onMouseUp={event => {
|
||||
onMouseDown={event => {
|
||||
if (event.button !== 0) {
|
||||
return; /* only left mouse clicks */
|
||||
}
|
||||
|
||||
events.fire("action_select", {
|
||||
entryIds: [ server.entryId ],
|
||||
mode: "auto",
|
||||
ignoreClientMove: false
|
||||
});
|
||||
this.props.server.select("auto");
|
||||
}}
|
||||
onContextMenu={event => {
|
||||
if (settings.static(Settings.KEY_DISABLE_CONTEXT_MENU)) {
|
||||
|
@ -56,8 +51,12 @@ export class ServerRenderer extends React.Component<{ server: RDPServer }, {}> {
|
|||
}
|
||||
|
||||
event.preventDefault();
|
||||
events.fire("action_show_context_menu", { treeEntryId: server.entryId, pageX: event.pageX, pageY: event.pageY });
|
||||
this.props.server.handleUiContextMenu(event.pageX, event.pageY);
|
||||
}}
|
||||
draggable={true}
|
||||
onDragStart={event => this.props.server.handleUiDragStart(event.nativeEvent)}
|
||||
onDragOver={event => this.props.server.handleUiDragOver(event.nativeEvent)}
|
||||
onDrop={event => this.props.server.handleUiDrop(event.nativeEvent)}
|
||||
>
|
||||
<div className={viewStyle.leftPadding} style={{ paddingLeft: server.offsetLeft + "em" }} />
|
||||
<UnreadMarkerRenderer entry={server} ref={server.refUnread} />
|
||||
|
|
|
@ -8,7 +8,6 @@ import * as React from "react";
|
|||
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
import {RDPChannelTree, RDPEntry} from "./RendererDataProvider";
|
||||
import {RendererMove} from "./RendererMove";
|
||||
import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
|
||||
import {ClientIcon} from "svg-sprites/client-icons";
|
||||
|
||||
|
@ -59,14 +58,6 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
|
|||
|
||||
private scrollFixRequested;
|
||||
|
||||
private mouseMove: { x: number, y: number, down: boolean, fired: boolean } = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
down: false,
|
||||
fired: false
|
||||
};
|
||||
private readonly documentMouseListener;
|
||||
|
||||
private inViewCallbacks: {
|
||||
index: number,
|
||||
callback: () => void,
|
||||
|
@ -84,16 +75,6 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
|
|||
tree: [],
|
||||
treeRevision: -1
|
||||
};
|
||||
|
||||
this.documentMouseListener = (e: MouseEvent) => {
|
||||
if (e.type !== "mouseleave" && e.button !== 0)
|
||||
return;
|
||||
|
||||
this.mouseMove.down = false;
|
||||
this.mouseMove.fired = false;
|
||||
|
||||
this.removeDocumentMouseListener();
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
|
@ -143,16 +124,6 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
|
|||
});
|
||||
}
|
||||
|
||||
private registerDocumentMouseListener() {
|
||||
document.addEventListener("mouseleave", this.documentMouseListener);
|
||||
document.addEventListener("mouseup", this.documentMouseListener);
|
||||
}
|
||||
|
||||
private removeDocumentMouseListener() {
|
||||
document.removeEventListener("mouseleave", this.documentMouseListener);
|
||||
document.removeEventListener("mouseup", this.documentMouseListener);
|
||||
}
|
||||
|
||||
private visibleEntries() {
|
||||
let viewEntryCount = Math.ceil(this.state.view_height / ChannelTreeView.EntryHeight);
|
||||
const viewEntryBegin = Math.floor(this.state.scroll_offset / ChannelTreeView.EntryHeight);
|
||||
|
@ -190,13 +161,11 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
|
|||
className={viewStyle.channelTreeContainer + " " + (this.state.smoothScroll ? viewStyle.smoothScroll : "")}
|
||||
onScroll={() => this.onScroll()}
|
||||
ref={this.refContainer}
|
||||
onMouseDown={e => this.onMouseDown(e)}
|
||||
onMouseMove={e => this.onMouseMove(e)}
|
||||
onContextMenu={event => {
|
||||
if(event.target !== this.refContainer.current) { return; }
|
||||
|
||||
event.preventDefault();
|
||||
this.props.events.fire("action_show_context_menu", { pageY: event.pageY, pageX: event.pageX, treeEntryId: 0 });
|
||||
this.props.events.fire("action_show_context_menu", { pageY: event.pageY, pageX: event.pageX, treeEntryIds: [] });
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
@ -204,16 +173,6 @@ 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>
|
||||
)
|
||||
}
|
||||
|
@ -224,34 +183,6 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
|
|||
});
|
||||
}
|
||||
|
||||
private onMouseDown(e: React.MouseEvent) {
|
||||
if (e.button !== 0) return; /* left button only */
|
||||
|
||||
this.mouseMove.down = true;
|
||||
this.mouseMove.x = e.pageX;
|
||||
this.mouseMove.y = e.pageY;
|
||||
this.registerDocumentMouseListener();
|
||||
}
|
||||
|
||||
private onMouseMove(e: React.MouseEvent) {
|
||||
if (!this.mouseMove.down || this.mouseMove.fired) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.abs((this.mouseMove.x - e.pageX) * (this.mouseMove.y - e.pageY)) > (this.props.moveThreshold || 9)) {
|
||||
const sourceEntry = this.getEntryFromPoint(this.mouseMove.x, this.mouseMove.y);
|
||||
if(!sourceEntry) { return; }
|
||||
|
||||
this.props.events.fire("action_select", { entryIds: [ sourceEntry ], mode: "auto-add", ignoreClientMove: true });
|
||||
|
||||
this.mouseMove.fired = true;
|
||||
this.props.events.fire("action_start_entry_move", {
|
||||
current: { x: e.pageX, y: e.pageY },
|
||||
start: { x: this.mouseMove.x, y: this.mouseMove.y }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
scrollEntryInView(entryId: number, callback?: () => void) {
|
||||
const index = this.state.tree.findIndex(e => e.entryId === entryId);
|
||||
if (index === -1) {
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
html:root {
|
||||
--channel-tree-move-color: hsla(220, 5%, 2%, 1);
|
||||
--channel-tree-move-background: hsla(0, 0%, 25%, 1);
|
||||
--channel-tree-move-border: hsla(220, 4%, 40%, 1);
|
||||
}
|
||||
|
||||
.moveContainer {
|
||||
position: absolute;
|
||||
display: block;
|
||||
|
||||
border: 2px solid var(--channel-tree-move-border);
|
||||
background-color: var(--channel-tree-move-background);
|
||||
|
||||
z-index: 10000;
|
||||
margin-left: 5px;
|
||||
|
||||
padding-left: .25em;
|
||||
padding-right: .25em;
|
||||
|
||||
border-radius: 2px;
|
||||
color: var(--channel-tree-move-color);
|
||||
}
|
|
@ -69,14 +69,17 @@ export class RtpVoiceConnection extends AbstractVoiceConnection {
|
|||
}
|
||||
});
|
||||
|
||||
/* FIXME: Listener for audio! */
|
||||
|
||||
this.listenerClientMoved = this.rtcConnection.getConnection().command_handler_boss().register_explicit_handler("notifyclientmoved", event => {
|
||||
const localClientId = this.rtcConnection.getConnection().client.getClientId();
|
||||
for(const data of event.arguments) {
|
||||
if(parseInt(data["clid"]) === localClientId) {
|
||||
/* TODO: Error handling if we failed to start */
|
||||
this.rtcConnection.startTrackBroadcast("audio");
|
||||
this.rtcConnection.startTrackBroadcast("audio").catch(error => {
|
||||
logError(LogCategory.VOICE, tr("Failed to start voice audio broadcasting after channel switch: %o"), error);
|
||||
this.localFailedReason = tr("Failed to start audio broadcasting");
|
||||
this.setConnectionState(VoiceConnectionStatus.Failed);
|
||||
}).catch(() => {
|
||||
this.setConnectionState(VoiceConnectionStatus.Connected);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue