Added channel movement and reordering
This commit is contained in:
parent
9a42a02227
commit
903c29ac51
20 changed files with 901 additions and 796 deletions
|
@ -2,6 +2,7 @@
|
||||||
* **03.12.20**
|
* **03.12.20**
|
||||||
- Fixed server connection tab move handler
|
- Fixed server connection tab move handler
|
||||||
- Fixed file browser context menu when having an overlay
|
- Fixed file browser context menu when having an overlay
|
||||||
|
- Added channel movement and reordering
|
||||||
|
|
||||||
* **02.12.20**
|
* **02.12.20**
|
||||||
- Fixed a bug within the client entry move mechanic of the channel tree which prevented any client selection
|
- 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;
|
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) {
|
showContextMenu(x: number, y: number, on_close: () => void = undefined) {
|
||||||
let channelCreate = !![
|
let channelCreate = !![
|
||||||
PermissionType.B_CHANNEL_CREATE_TEMPORARY,
|
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 {EventType} from "tc-shared/ui/frames/log/Definitions";
|
||||||
import {renderChannelTree} from "tc-shared/ui/tree/Controller";
|
import {renderChannelTree} from "tc-shared/ui/tree/Controller";
|
||||||
import {ChannelTreePopoutController} from "tc-shared/ui/tree/popout/Controller";
|
import {ChannelTreePopoutController} from "tc-shared/ui/tree/popout/Controller";
|
||||||
|
import {Settings, settings} from "tc-shared/settings";
|
||||||
|
|
||||||
export interface ChannelTreeEvents {
|
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 */
|
/* general tree notified */
|
||||||
notify_tree_reset: {},
|
notify_tree_reset: {},
|
||||||
notify_selection_changed: {},
|
|
||||||
notify_query_view_state_changed: { queries_shown: boolean },
|
notify_query_view_state_changed: { queries_shown: boolean },
|
||||||
notify_popout_state_changed: { popoutShown: boolean },
|
notify_popout_state_changed: { popoutShown: boolean },
|
||||||
|
|
||||||
|
@ -80,270 +69,11 @@ export interface ChannelTreeEvents {
|
||||||
message?: string,
|
message?: string,
|
||||||
isServerLeave: boolean,
|
isServerLeave: boolean,
|
||||||
sourceChannel: ChannelEntry
|
sourceChannel: ChannelEntry
|
||||||
}
|
},
|
||||||
}
|
|
||||||
|
|
||||||
export class ChannelTreeEntrySelect {
|
notify_selected_entry_changed: {
|
||||||
readonly handle: ChannelTree;
|
oldEntry: ChannelTreeEntry<any> | undefined,
|
||||||
selectedEntries: ChannelTreeEntry<any>[] = [];
|
newEntry: ChannelTreeEntry<any> | undefined
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -359,14 +89,14 @@ export class ChannelTree {
|
||||||
/* whatever all channels have been initialized */
|
/* whatever all channels have been initialized */
|
||||||
channelsInitialized: boolean = false;
|
channelsInitialized: boolean = false;
|
||||||
|
|
||||||
readonly selection: ChannelTreeEntrySelect;
|
|
||||||
readonly popoutController: ChannelTreePopoutController;
|
readonly popoutController: ChannelTreePopoutController;
|
||||||
|
|
||||||
private readonly tagContainer: JQuery;
|
private readonly tagContainer: JQuery;
|
||||||
|
|
||||||
private _show_queries: boolean;
|
private selectedEntry: ChannelTreeEntry<any> | undefined;
|
||||||
private channel_last?: ChannelEntry;
|
private showQueries: boolean;
|
||||||
private channel_first?: ChannelEntry;
|
private channelLast?: ChannelEntry;
|
||||||
|
private channelFirst?: ChannelEntry;
|
||||||
|
|
||||||
constructor(client) {
|
constructor(client) {
|
||||||
this.events = new Registry<ChannelTreeEvents>();
|
this.events = new Registry<ChannelTreeEvents>();
|
||||||
|
@ -375,7 +105,6 @@ export class ChannelTree {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
|
|
||||||
this.server = new ServerEntry(this, "undefined", undefined);
|
this.server = new ServerEntry(this, "undefined", undefined);
|
||||||
this.selection = new ChannelTreeEntrySelect(this);
|
|
||||||
this.popoutController = new ChannelTreePopoutController(this);
|
this.popoutController = new ChannelTreePopoutController(this);
|
||||||
|
|
||||||
this.tagContainer = $.spawn("div").addClass("channel-tree-container");
|
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.child_channel_head && visit(channel.child_channel_head);
|
||||||
channel.channel_next && visit(channel.channel_next);
|
channel.channel_next && visit(channel.channel_next);
|
||||||
};
|
};
|
||||||
this.channel_first && visit(this.channel_first);
|
this.channelFirst && visit(this.channelFirst);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -420,6 +149,40 @@ export class ChannelTree {
|
||||||
return undefined;
|
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() {
|
destroy() {
|
||||||
ReactDOM.unmountComponentAtNode(this.tagContainer[0]);
|
ReactDOM.unmountComponentAtNode(this.tagContainer[0]);
|
||||||
|
|
||||||
|
@ -429,12 +192,11 @@ export class ChannelTree {
|
||||||
}
|
}
|
||||||
this.reset(); /* cleanup channel and clients */
|
this.reset(); /* cleanup channel and clients */
|
||||||
|
|
||||||
this.channel_first = undefined;
|
this.channelFirst = undefined;
|
||||||
this.channel_last = undefined;
|
this.channelLast = undefined;
|
||||||
|
|
||||||
this.popoutController.destroy();
|
this.popoutController.destroy();
|
||||||
this.tagContainer.remove();
|
this.tagContainer.remove();
|
||||||
this.selection.destroy();
|
|
||||||
this.events.destroy();
|
this.events.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -446,7 +208,7 @@ export class ChannelTree {
|
||||||
|
|
||||||
rootChannel() : ChannelEntry[] {
|
rootChannel() : ChannelEntry[] {
|
||||||
const result = [];
|
const result = [];
|
||||||
let first = this.channel_first;
|
let first = this.channelFirst;
|
||||||
while(first) {
|
while(first) {
|
||||||
result.push(first);
|
result.push(first);
|
||||||
first = first.channel_next;
|
first = first.channel_next;
|
||||||
|
@ -455,6 +217,10 @@ export class ChannelTree {
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteChannel(channel: ChannelEntry) {
|
deleteChannel(channel: ChannelEntry) {
|
||||||
|
if(this.selectedEntry === channel) {
|
||||||
|
this.setSelectedEntry(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
channel.channelTree = null;
|
channel.channelTree = null;
|
||||||
|
|
||||||
batch_updates(BatchUpdateType.CHANNEL_TREE);
|
batch_updates(BatchUpdateType.CHANNEL_TREE);
|
||||||
|
@ -517,12 +283,12 @@ export class ChannelTree {
|
||||||
channel.channel_next.channel_previous = channel.channel_previous;
|
channel.channel_next.channel_previous = channel.channel_previous;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(channel === this.channel_last) {
|
if(channel === this.channelLast) {
|
||||||
this.channel_last = channel.channel_previous;
|
this.channelLast = channel.channel_previous;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(channel === this.channel_first) {
|
if(channel === this.channelFirst) {
|
||||||
this.channel_first = channel.channel_next;
|
this.channelFirst = channel.channel_next;
|
||||||
}
|
}
|
||||||
|
|
||||||
channel.channel_next = undefined;
|
channel.channel_next = undefined;
|
||||||
|
@ -542,8 +308,8 @@ export class ChannelTree {
|
||||||
channel.parent = parent;
|
channel.parent = parent;
|
||||||
|
|
||||||
if(channelPrevious) {
|
if(channelPrevious) {
|
||||||
if(channelPrevious == this.channel_last) {
|
if(channelPrevious == this.channelLast) {
|
||||||
this.channel_last = channel;
|
this.channelLast = channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
channel.channel_next = channelPrevious.channel_next;
|
channel.channel_next = channelPrevious.channel_next;
|
||||||
|
@ -563,12 +329,13 @@ export class ChannelTree {
|
||||||
channel.channel_next.channel_previous = channel;
|
channel.channel_next.channel_previous = channel;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel.channel_next = this.channel_first;
|
channel.channel_next = this.channelFirst;
|
||||||
if(this.channel_first)
|
if(this.channelFirst) {
|
||||||
this.channel_first.channel_previous = channel;
|
this.channelFirst.channel_previous = channel;
|
||||||
|
}
|
||||||
|
|
||||||
this.channel_first = channel;
|
this.channelFirst = channel;
|
||||||
this.channel_last = this.channel_last || channel;
|
this.channelLast = this.channelLast || channel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -587,6 +354,10 @@ export class ChannelTree {
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteClient(client: ClientEntry, reason: { reason: ViewReasonId, message?: string, serverLeave: boolean }) {
|
deleteClient(client: ClientEntry, reason: { reason: ViewReasonId, message?: string, serverLeave: boolean }) {
|
||||||
|
if(this.selectedEntry === client) {
|
||||||
|
this.setSelectedEntry(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
const oldChannel = client.currentChannel();
|
const oldChannel = client.currentChannel();
|
||||||
oldChannel?.unregisterClient(client);
|
oldChannel?.unregisterClient(client);
|
||||||
this.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 clients = entries.filter(e => e instanceof ClientEntry) as ClientEntry[];
|
||||||
const channels = entries.filter(e => e instanceof ChannelEntry) as ChannelEntry[];
|
const channels = entries.filter(e => e instanceof ChannelEntry) as ChannelEntry[];
|
||||||
const server = entries.find(e => e instanceof ServerEntry) as ServerEntry;
|
const server = entries.find(e => e instanceof ServerEntry) as ServerEntry;
|
||||||
|
@ -771,13 +542,12 @@ export class ChannelTree {
|
||||||
callback: () => {
|
callback: () => {
|
||||||
createInputModal(tr("Poke clients"), tr("Poke message:<br>"), text => true, result => {
|
createInputModal(tr("Poke clients"), tr("Poke message:<br>"), text => true, result => {
|
||||||
if (typeof(result) === "string") {
|
if (typeof(result) === "string") {
|
||||||
for (const client of clients)
|
for (const client of clients) {
|
||||||
this.client.serverConnection.send_command("clientpoke", {
|
this.client.serverConnection.send_command("clientpoke", {
|
||||||
clid: client.clientId(),
|
clid: client.clientId(),
|
||||||
msg: result
|
msg: result
|
||||||
});
|
});
|
||||||
|
}
|
||||||
this.selection.clearSelection();
|
|
||||||
}
|
}
|
||||||
}, {width: 400, maxLength: 512}).open();
|
}, {width: 400, maxLength: 512}).open();
|
||||||
}
|
}
|
||||||
|
@ -789,12 +559,12 @@ export class ChannelTree {
|
||||||
name: tr("Move clients to your channel"),
|
name: tr("Move clients to your channel"),
|
||||||
callback: () => {
|
callback: () => {
|
||||||
const target = this.client.getClient().currentChannel().getChannelId();
|
const target = this.client.getClient().currentChannel().getChannelId();
|
||||||
for(const client of clients)
|
for(const client of clients) {
|
||||||
this.client.serverConnection.send_command("clientmove", {
|
this.client.serverConnection.send_command("clientmove", {
|
||||||
clid: client.clientId(),
|
clid: client.clientId(),
|
||||||
cid: target
|
cid: target
|
||||||
});
|
});
|
||||||
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
|
||||||
|
@ -814,7 +584,6 @@ export class ChannelTree {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, {width: 400, maxLength: 255}).open();
|
}, {width: 400, maxLength: 255}).open();
|
||||||
this.selection.clearSelection();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -824,7 +593,6 @@ export class ChannelTree {
|
||||||
icon_class: "client-poke",
|
icon_class: "client-poke",
|
||||||
name: tr("Poke clients"),
|
name: tr("Poke clients"),
|
||||||
callback: () => {
|
callback: () => {
|
||||||
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 });
|
||||||
|
@ -838,7 +606,6 @@ 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.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)
|
||||||
|
@ -856,7 +623,6 @@ 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.clearSelection();
|
|
||||||
spawnBanClient(this.client, (clients).map(entry => {
|
spawnBanClient(this.client, (clients).map(entry => {
|
||||||
return {
|
return {
|
||||||
name: entry.clientNickName(),
|
name: entry.clientNickName(),
|
||||||
|
@ -890,11 +656,11 @@ export class ChannelTree {
|
||||||
const tag_container = $.spawn("div").append(tag);
|
const tag_container = $.spawn("div").append(tag);
|
||||||
spawnYesNo(tr("Are you sure?"), tag_container, result => {
|
spawnYesNo(tr("Are you sure?"), tag_container, result => {
|
||||||
if(result) {
|
if(result) {
|
||||||
for(const client of clients)
|
for(const client of clients) {
|
||||||
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.clearSelection();
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -914,16 +680,17 @@ export class ChannelTree {
|
||||||
callback: () => {
|
callback: () => {
|
||||||
spawnYesNo(tr("Are you sure?"), tra("Do you really want to delete {0} channels?", channels.length), result => {
|
spawnYesNo(tr("Are you sure?"), tra("Do you really want to delete {0} channels?", channels.length), result => {
|
||||||
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.clearSelection();
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if(server)
|
if(server) {
|
||||||
server_menu = server.contextMenuItems();
|
server_menu = server.contextMenuItems();
|
||||||
|
}
|
||||||
|
|
||||||
const menus = [
|
const menus = [
|
||||||
{
|
{
|
||||||
|
@ -982,9 +749,8 @@ export class ChannelTree {
|
||||||
this.channelsInitialized = false;
|
this.channelsInitialized = false;
|
||||||
batch_updates(BatchUpdateType.CHANNEL_TREE);
|
batch_updates(BatchUpdateType.CHANNEL_TREE);
|
||||||
|
|
||||||
this.selection.clearSelection();
|
|
||||||
try {
|
try {
|
||||||
this.selection.reset();
|
this.setSelectedEntry(undefined);
|
||||||
|
|
||||||
const voiceConnection = this.client.serverConnection ? this.client.serverConnection.getVoiceConnection() : undefined;
|
const voiceConnection = this.client.serverConnection ? this.client.serverConnection.getVoiceConnection() : undefined;
|
||||||
const videoConnection = this.client.serverConnection ? this.client.serverConnection.getVideoConnection() : undefined;
|
const videoConnection = this.client.serverConnection ? this.client.serverConnection.getVideoConnection() : undefined;
|
||||||
|
@ -1006,8 +772,8 @@ export class ChannelTree {
|
||||||
channel.destroy();
|
channel.destroy();
|
||||||
|
|
||||||
this.channels = [];
|
this.channels = [];
|
||||||
this.channel_last = undefined;
|
this.channelLast = undefined;
|
||||||
this.channel_first = undefined;
|
this.channelFirst = undefined;
|
||||||
this.events.fire("notify_tree_reset");
|
this.events.fire("notify_tree_reset");
|
||||||
} finally {
|
} finally {
|
||||||
flush_batched_updates(BatchUpdateType.CHANNEL_TREE);
|
flush_batched_updates(BatchUpdateType.CHANNEL_TREE);
|
||||||
|
@ -1054,15 +820,15 @@ export class ChannelTree {
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle_server_queries(flag: boolean) {
|
toggle_server_queries(flag: boolean) {
|
||||||
if(this._show_queries == flag) return;
|
if(this.showQueries == flag) return;
|
||||||
this._show_queries = flag;
|
this.showQueries = flag;
|
||||||
|
|
||||||
this.events.fire("notify_query_view_state_changed", { queries_shown: 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 {
|
get_first_channel?() : ChannelEntry {
|
||||||
return this.channel_first;
|
return this.channelFirst;
|
||||||
}
|
}
|
||||||
|
|
||||||
unsubscribe_all_channels(subscribe_specified?: boolean) {
|
unsubscribe_all_channels(subscribe_specified?: boolean) {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import {Registry} from "../events";
|
import {Registry} from "../events";
|
||||||
|
|
||||||
export interface ChannelTreeEntryEvents {
|
export interface ChannelTreeEntryEvents {
|
||||||
notify_select_state_change: { selected: boolean },
|
|
||||||
notify_unread_state_change: { unread: boolean }
|
notify_unread_state_change: { unread: boolean }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,24 +16,6 @@ export abstract class ChannelTreeEntry<Events extends ChannelTreeEntryEvents> {
|
||||||
this.uniqueEntryId = ++treeEntryIdCounter;
|
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) {
|
setUnread(flag: boolean) {
|
||||||
if(this.unread_ === flag) return;
|
if(this.unread_ === flag) return;
|
||||||
this.unread_ = flag;
|
this.unread_ = flag;
|
||||||
|
|
|
@ -372,56 +372,6 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
||||||
protected initializeListener() {
|
protected initializeListener() {
|
||||||
if(this._listener_initialized) return;
|
if(this._listener_initialized) return;
|
||||||
this._listener_initialized = true;
|
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[] {
|
protected contextmenu_info() : contextmenu.MenuEntry[] {
|
||||||
|
|
|
@ -170,18 +170,6 @@ export class ServerEntry extends ChannelTreeEntry<ServerEvents> {
|
||||||
this.remote_address = undefined;
|
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[] {
|
contextMenuItems() : contextmenu.MenuEntry[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,12 +4,12 @@ import {FileType} from "../../../file/FileManager";
|
||||||
import {CommandResult} from "../../../connection/ServerConnectionDeclaration";
|
import {CommandResult} from "../../../connection/ServerConnectionDeclaration";
|
||||||
import PermissionType from "../../../permission/PermissionType";
|
import PermissionType from "../../../permission/PermissionType";
|
||||||
import * as log from "../../../log";
|
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 {Entry, MenuEntry, MenuEntryType, spawn_context_menu} from "../../../ui/elements/ContextMenu";
|
||||||
import * as ppt from "tc-backend/ppt";
|
import * as ppt from "tc-backend/ppt";
|
||||||
import {SpecialKey} from "../../../PPTListener";
|
import {SpecialKey} from "../../../PPTListener";
|
||||||
import {spawnYesNo} from "../../../ui/modal/ModalYesNo";
|
import {spawnYesNo} from "../../../ui/modal/ModalYesNo";
|
||||||
import {tra, traj, tr} from "../../../i18n/localize";
|
import {tr, tra, traj} from "../../../i18n/localize";
|
||||||
import {
|
import {
|
||||||
FileTransfer,
|
FileTransfer,
|
||||||
FileTransferState,
|
FileTransferState,
|
||||||
|
@ -105,6 +105,8 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
logTrace(LogCategory.FILE_TRANSFER, tr("Requesting a file list for %o"), path);
|
||||||
|
|
||||||
let request: Promise<ListedFileInfo[]>;
|
let request: Promise<ListedFileInfo[]>;
|
||||||
if (path.type === "root") {
|
if (path.type === "root") {
|
||||||
request = (async () => {
|
request = (async () => {
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
html:root {
|
||||||
|
--channel-tree-move-border: #005fa1;
|
||||||
|
}
|
||||||
|
|
||||||
.channelEntry {
|
.channelEntry {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@ -110,4 +114,58 @@
|
||||||
z-index: -1;
|
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.channels.forEach(channel => this.initializeChannelEvents(channel));
|
||||||
this.channelTree.clients.forEach(channel => this.initializeClientEvents(channel));
|
this.channelTree.clients.forEach(channel => this.initializeClientEvents(channel));
|
||||||
this.sendChannelTreeEntries();
|
this.sendChannelTreeEntries();
|
||||||
|
this.sendSelectedEntry();
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler<ChannelTreeEvents>("notify_channel_created")
|
@EventHandler<ChannelTreeEvents>("notify_channel_created")
|
||||||
|
@ -250,15 +251,18 @@ class ChannelTreeController {
|
||||||
this.sendChannelTreeEntries();
|
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 */
|
/* entry event handlers */
|
||||||
private initializeTreeEntryEvents<T extends ChannelTreeEntryEvents>(entry: ChannelTreeEntryModel<T>, events: any[]) {
|
private initializeTreeEntryEvents<T extends ChannelTreeEntryEvents>(entry: ChannelTreeEntryModel<T>, events: any[]) {
|
||||||
events.push(entry.events.on("notify_unread_state_change", event => {
|
events.push(entry.events.on("notify_unread_state_change", event => {
|
||||||
this.events.fire("notify_unread_state", { unread: event.unread, treeEntryId: entry.uniqueEntryId });
|
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) {
|
private initializeChannelEvents(channel: ChannelEntry) {
|
||||||
|
@ -387,6 +391,11 @@ class ChannelTreeController {
|
||||||
this.events.fire_react("notify_tree_entries", { entries: entries });
|
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) {
|
public sendChannelInfo(channel: ChannelEntry) {
|
||||||
this.events.fire_react("notify_channel_info", {
|
this.events.fire_react("notify_channel_info", {
|
||||||
treeEntryId: channel.uniqueEntryId,
|
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.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("notify_destroy", channelTree.client.events().on("notify_visibility_changed", event => events.fire("notify_visibility_changed", event)));
|
||||||
|
|
||||||
events.on("query_tree_entries", () => controller.sendChannelTreeEntries());
|
events.on("query_tree_entries", () => controller.sendChannelTreeEntries());
|
||||||
|
@ -663,67 +662,43 @@ export function initializeChannelTreeController(events: Registry<ChannelTreeUIEv
|
||||||
});
|
});
|
||||||
|
|
||||||
events.on("action_select", event => {
|
events.on("action_select", event => {
|
||||||
console.error("Select mode: %o", moveSelection);
|
if(event.treeEntryId === 0) {
|
||||||
if(!event.ignoreClientMove && moveSelection?.length) {
|
channelTree.setSelectedEntry(undefined);
|
||||||
console.error("X");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries = [];
|
const entry = channelTree.findEntryId(event.treeEntryId);
|
||||||
for(const entryId of event.entryIds) {
|
if(!entry) {
|
||||||
const entry = channelTree.findEntryId(entryId);
|
logWarn(LogCategory.CHANNEL, tr("Tried to select an invalid channel tree entry with id %o"), event.treeEntryId);
|
||||||
if(!entry) {
|
return;
|
||||||
logWarn(LogCategory.CHANNEL, tr("Tried to select an invalid tree entry with id %o. Skipping entry."), entryId);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
entries.push(entry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
channelTree.selection.select(entries, event.mode);
|
channelTree.setSelectedEntry(entry);
|
||||||
});
|
|
||||||
|
|
||||||
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 => {
|
||||||
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);
|
channelTree.showContextMenu(event.pageX, event.pageY);
|
||||||
return;
|
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 => {
|
events.on("action_channel_join", event => {
|
||||||
if(!event.ignoreMultiSelect && channelTree.selection.isMultiSelect()) {
|
const entry = channelTree.findEntryId(event.treeEntryId);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = event.treeEntryId === "selected" ? channelTree.selection.selectedEntries[0] : channelTree.findEntryId(event.treeEntryId);
|
|
||||||
if(!entry || !(entry instanceof ChannelEntry)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -737,15 +712,7 @@ export function initializeChannelTreeController(events: Registry<ChannelTreeUIEv
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(moveSelection) {
|
channelTree.setSelectedEntry(entry);
|
||||||
/* don't select entries while we're moving */
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
channelTree.events.fire("action_select_entries", {
|
|
||||||
entries: [entry],
|
|
||||||
mode: "exclusive"
|
|
||||||
});
|
|
||||||
spawnFileTransferModal(entry.channelId);
|
spawnFileTransferModal(entry.channelId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -756,10 +723,6 @@ export function initializeChannelTreeController(events: Registry<ChannelTreeUIEv
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(channelTree.selection.isMultiSelect()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry instanceof LocalClientEntry) {
|
if (entry instanceof LocalClientEntry) {
|
||||||
entry.openRename(events);
|
entry.openRename(events);
|
||||||
} else if (entry instanceof MusicClientEntry) {
|
} else if (entry instanceof MusicClientEntry) {
|
||||||
|
@ -786,46 +749,111 @@ export function initializeChannelTreeController(events: Registry<ChannelTreeUIEv
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let moveSelection: ClientEntry[];
|
events.on("action_move_clients", event => {
|
||||||
events.on("action_start_entry_move", event => {
|
const entry = channelTree.findEntryId(event.targetTreeEntry);
|
||||||
const selection = channelTree.selection.selectedEntries.slice();
|
if(!entry) {
|
||||||
if(selection.length === 0) { return; }
|
logWarn(LogCategory.CHANNEL, tr("Received client move notify with an unknown target entry id %o"), event.targetTreeEntry);
|
||||||
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;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = channelTree.findEntryId(event.treeEntryId);
|
|
||||||
|
|
||||||
|
|
||||||
let targetChannel: ChannelEntry;
|
let targetChannel: ChannelEntry;
|
||||||
if(entry instanceof ChannelEntry) {
|
if(entry instanceof ClientEntry) {
|
||||||
targetChannel = entry;
|
|
||||||
} else if(entry instanceof ClientEntry) {
|
|
||||||
targetChannel = entry.currentChannel();
|
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) {
|
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);
|
/* should not happen often that a client hasn't a channel */
|
||||||
moveSelection = undefined;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
moveSelection.filter(e => e.currentChannel() !== entry).forEach(e => {
|
const clients = event.entries.map(entryId => {
|
||||||
channelTree.client.serverConnection.send_command("clientmove", {
|
const entry = channelTree.findEntryId(entryId);
|
||||||
clid: e.clientId(),
|
if(!entry || !(entry instanceof ClientEntry)) {
|
||||||
cid: targetChannel.channelId
|
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 => {
|
events.on("notify_client_name_edit_failed", event => {
|
||||||
|
|
|
@ -29,27 +29,23 @@ export type ServerState = { state: "disconnected" } | { state: "connecting", tar
|
||||||
export interface ChannelTreeUIEvents {
|
export interface ChannelTreeUIEvents {
|
||||||
/* actions */
|
/* actions */
|
||||||
action_toggle_popout: { shown: boolean },
|
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_start_entry_move: { start: { x: number, y: number }, current: { x: number, y: number } },
|
||||||
action_set_collapsed_state: { treeEntryId: number, state: "collapsed" | "expended" },
|
action_set_collapsed_state: { treeEntryId: number, state: "collapsed" | "expended" },
|
||||||
action_select: {
|
action_select: { treeEntryId: number | 0 },
|
||||||
entryIds: number[],
|
action_channel_join: { treeEntryId: 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_channel_open_file_browser: { treeEntryId: number },
|
action_channel_open_file_browser: { treeEntryId: number },
|
||||||
action_client_double_click: { treeEntryId: number },
|
action_client_double_click: { treeEntryId: number },
|
||||||
action_client_name_submit: { treeEntryId: number, name: string },
|
action_client_name_submit: { treeEntryId: number, name: string },
|
||||||
action_move_entries: { treeEntryId: number /* zero if move failed */ }
|
action_move_clients: { targetTreeEntry: number, entries: number[] },
|
||||||
|
action_move_channels: { targetTreeEntry: number, mode: "before" | "after" | "child", entries: number[] },
|
||||||
|
|
||||||
/* queries */
|
/* queries */
|
||||||
query_tree_entries: {},
|
query_tree_entries: {},
|
||||||
query_popout_state: {},
|
query_popout_state: {},
|
||||||
|
query_selected_entry: {},
|
||||||
|
|
||||||
query_unread_state: { treeEntryId: number },
|
query_unread_state: { treeEntryId: number },
|
||||||
query_select_state: { treeEntryId: number },
|
|
||||||
|
|
||||||
query_channel_info: { treeEntryId: number },
|
query_channel_info: { treeEntryId: number },
|
||||||
query_channel_icon: { treeEntryId: number },
|
query_channel_icon: { treeEntryId: number },
|
||||||
|
@ -65,6 +61,7 @@ export interface ChannelTreeUIEvents {
|
||||||
/* notifies */
|
/* notifies */
|
||||||
notify_tree_entries: { entries: ChannelTreeEntry[] },
|
notify_tree_entries: { entries: ChannelTreeEntry[] },
|
||||||
notify_popout_state: { shown: boolean, showButton: boolean },
|
notify_popout_state: { shown: boolean, showButton: boolean },
|
||||||
|
notify_selected_entry: { treeEntryId: number | 0 },
|
||||||
|
|
||||||
notify_channel_info: { treeEntryId: number, info: ChannelEntryInfo },
|
notify_channel_info: { treeEntryId: number, info: ChannelEntryInfo },
|
||||||
notify_channel_icon: { treeEntryId: number, icon: ClientIcon },
|
notify_channel_icon: { treeEntryId: number, icon: ClientIcon },
|
||||||
|
@ -80,10 +77,16 @@ export interface ChannelTreeUIEvents {
|
||||||
notify_server_state: { treeEntryId: number, state: ServerState },
|
notify_server_state: { treeEntryId: number, state: ServerState },
|
||||||
|
|
||||||
notify_unread_state: { treeEntryId: number, unread: boolean },
|
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_visibility_changed: { visible: boolean },
|
||||||
notify_destroy: {}
|
notify_destroy: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ChannelTreeDragData = {
|
||||||
|
version: 1,
|
||||||
|
handlerId: string,
|
||||||
|
type: string,
|
||||||
|
|
||||||
|
entryIds: number[],
|
||||||
|
entryTypes: ("server" | "channel" | "client")[]
|
||||||
|
};
|
188
shared/js/ui/tree/DragHelper.ts
Normal file
188
shared/js/ui/tree/DragHelper.ts
Normal file
|
@ -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 {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {ChannelTreeView, PopoutButton} from "tc-shared/ui/tree/RendererView";
|
import {ChannelTreeView, PopoutButton} from "tc-shared/ui/tree/RendererView";
|
||||||
import {RDPChannelTree} from "./RendererDataProvider";
|
import {RDPChannel, RDPChannelTree} from "./RendererDataProvider";
|
||||||
import {useEffect, useRef} from "react";
|
import {useEffect, useRef} from "react";
|
||||||
|
|
||||||
const viewStyle = require("./View.scss");
|
const viewStyle = require("./View.scss");
|
||||||
|
@ -34,13 +34,17 @@ const ContainerView = (props: { tree: RDPChannelTree, events: Registry<ChannelTr
|
||||||
|
|
||||||
if(event.key === "ArrowUp") {
|
if(event.key === "ArrowUp") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
props.events.fire("action_select_auto", { direction: "previous" });
|
props.tree.selection.selectNext(true, "up");
|
||||||
} else if(event.key === "ArrowDown") {
|
} else if(event.key === "ArrowDown") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
props.events.fire("action_select_auto", { direction: "next" });
|
props.tree.selection.selectNext(true, "down");
|
||||||
} else if(event.key === "Enter") {
|
} else if(event.key === "Enter") {
|
||||||
event.preventDefault();
|
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 (
|
return (
|
||||||
<div
|
<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 }}
|
style={{ top: channel.offsetTop }}
|
||||||
onMouseUp={event => {
|
onMouseUp={event => {
|
||||||
if (event.button !== 0) {
|
if (event.button !== 0) {
|
||||||
return; /* only left mouse clicks */
|
return; /* only left mouse clicks */
|
||||||
}
|
}
|
||||||
|
|
||||||
events.fire("action_select", {
|
this.props.channel.select("auto");
|
||||||
entryIds: [ entryId ],
|
|
||||||
mode: "auto",
|
|
||||||
ignoreClientMove: false
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
onDoubleClick={() => events.fire("action_channel_join", { ignoreMultiSelect: false, treeEntryId: entryId })}
|
onDoubleClick={() => events.fire("action_channel_join", { treeEntryId: entryId })}
|
||||||
onContextMenu={event => {
|
onContextMenu={event => {
|
||||||
if (settings.static(Settings.KEY_DISABLE_CONTEXT_MENU)) {
|
if (settings.static(Settings.KEY_DISABLE_CONTEXT_MENU)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
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 => {
|
onMouseDown={event => {
|
||||||
if (event.buttons !== 4) {
|
if (event.buttons !== 4) {
|
||||||
|
@ -155,8 +157,12 @@ export class RendererChannel extends React.Component<{ channel: RDPChannel }, {}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
events.fire("action_channel_open_file_browser", { treeEntryId: entryId });
|
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} />
|
<UnreadMarkerRenderer entry={this.props.channel} ref={this.props.channel.refUnread} />
|
||||||
{collapsedIndicator}
|
{collapsedIndicator}
|
||||||
{channelIcon}
|
{channelIcon}
|
||||||
|
|
|
@ -183,27 +183,27 @@ export class RendererClient extends React.Component<{ client: RDPClient }, {}> {
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
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 => {
|
onMouseUp={event => {
|
||||||
if (event.button !== 0) {
|
if (event.button !== 0) {
|
||||||
return; /* only left mouse clicks */
|
return; /* only left mouse clicks */
|
||||||
}
|
}
|
||||||
|
|
||||||
events.fire("action_select", {
|
this.props.client.select("auto");
|
||||||
entryIds: [ client.entryId ],
|
|
||||||
mode: "auto",
|
|
||||||
ignoreClientMove: false
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
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" }} />
|
<div className={viewStyle.leftPadding} style={{ paddingLeft: client.offsetLeft + "em" }} />
|
||||||
<UnreadMarkerRenderer entry={client} ref={client.refUnread} />
|
<UnreadMarkerRenderer entry={client} ref={client.refUnread} />
|
||||||
<ClientStatus client={client} ref={client.refStatus} />
|
<ClientStatus client={client} ref={client.refStatus} />
|
||||||
{...(client.rename ? [
|
{...(client.rename ? [
|
||||||
<ClientNameEdit initialName={client.renameDefault} editFinished={value => {
|
<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"} />
|
}} key={"rename"} />
|
||||||
] : [
|
] : [
|
||||||
<ClientName client={client} ref={client.refName} key={"name"} />,
|
<ClientName client={client} ref={client.refName} key={"name"} />,
|
||||||
|
|
|
@ -4,7 +4,9 @@ import {
|
||||||
ChannelIcons,
|
ChannelIcons,
|
||||||
ChannelTreeUIEvents,
|
ChannelTreeUIEvents,
|
||||||
ClientIcons,
|
ClientIcons,
|
||||||
ClientNameInfo, ClientTalkIconState, ServerState
|
ClientNameInfo,
|
||||||
|
ClientTalkIconState,
|
||||||
|
ServerState
|
||||||
} from "tc-shared/ui/tree/Definitions";
|
} from "tc-shared/ui/tree/Definitions";
|
||||||
import {ChannelTreeView, PopoutButton} from "tc-shared/ui/tree/RendererView";
|
import {ChannelTreeView, PopoutButton} from "tc-shared/ui/tree/RendererView";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
@ -20,7 +22,8 @@ import {
|
||||||
RendererClient
|
RendererClient
|
||||||
} from "tc-shared/ui/tree/RendererClient";
|
} from "tc-shared/ui/tree/RendererClient";
|
||||||
import {ServerRenderer} from "tc-shared/ui/tree/RendererServer";
|
import {ServerRenderer} from "tc-shared/ui/tree/RendererServer";
|
||||||
import {RendererMove} from "./RendererMove";
|
import {generateDragElement, getDragInfo, parseDragData, setupDragData} from "tc-shared/ui/tree/DragHelper";
|
||||||
|
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
||||||
|
|
||||||
function isEquivalent(a, b) {
|
function isEquivalent(a, b) {
|
||||||
const typeA = typeof a;
|
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 {
|
export class RDPChannelTree {
|
||||||
readonly events: Registry<ChannelTreeUIEvents>;
|
readonly events: Registry<ChannelTreeUIEvents>;
|
||||||
readonly handlerId: string;
|
readonly handlerId: string;
|
||||||
|
|
||||||
private registeredEventHandlers = [];
|
private registeredEventHandlers = [];
|
||||||
|
|
||||||
readonly refMove = React.createRef<RendererMove>();
|
|
||||||
readonly refTree = React.createRef<ChannelTreeView>();
|
readonly refTree = React.createRef<ChannelTreeView>();
|
||||||
readonly refPopoutButton = React.createRef<PopoutButton>();
|
readonly refPopoutButton = React.createRef<PopoutButton>();
|
||||||
|
|
||||||
|
readonly selection: RDPTreeSelection;
|
||||||
|
|
||||||
popoutShown: boolean = false;
|
popoutShown: boolean = false;
|
||||||
popoutButtonShown: boolean = false;
|
popoutButtonShown: boolean = false;
|
||||||
|
|
||||||
|
@ -78,9 +257,12 @@ export class RDPChannelTree {
|
||||||
private orderedTree: RDPEntry[] = [];
|
private orderedTree: RDPEntry[] = [];
|
||||||
private treeEntries: {[key: number]: RDPEntry} = {};
|
private treeEntries: {[key: number]: RDPEntry} = {};
|
||||||
|
|
||||||
|
private dragOverChannelEntry: RDPChannel;
|
||||||
|
|
||||||
constructor(events: Registry<ChannelTreeUIEvents>, handlerId: string) {
|
constructor(events: Registry<ChannelTreeUIEvents>, handlerId: string) {
|
||||||
this.events = events;
|
this.events = events;
|
||||||
this.handlerId = handlerId;
|
this.handlerId = handlerId;
|
||||||
|
this.selection = new RDPTreeSelection(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
|
@ -98,17 +280,6 @@ export class RDPChannelTree {
|
||||||
entry.handleUnreadUpdate(event.unread);
|
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 => {
|
events.push(this.events.on("notify_channel_info", event => {
|
||||||
const entry = this.treeEntries[event.treeEntryId];
|
const entry = this.treeEntries[event.treeEntryId];
|
||||||
if(!entry || !(entry instanceof RDPChannel)) {
|
if(!entry || !(entry instanceof RDPChannel)) {
|
||||||
|
@ -201,8 +372,17 @@ export class RDPChannelTree {
|
||||||
entry.handleStateUpdate(event.state);
|
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_tree_entries");
|
||||||
this.events.fire("query_popout_state");
|
this.events.fire("query_popout_state");
|
||||||
|
this.events.fire("query_selected_entry");
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
@ -215,6 +395,129 @@ export class RDPChannelTree {
|
||||||
return this.orderedTree;
|
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")
|
@EventHandler<ChannelTreeUIEvents>("notify_tree_entries")
|
||||||
private handleNotifyTreeEntries(event: ChannelTreeUIEvents["notify_tree_entries"]) {
|
private handleNotifyTreeEntries(event: ChannelTreeUIEvents["notify_tree_entries"]) {
|
||||||
const oldEntryInstances = this.treeEntries;
|
const oldEntryInstances = this.treeEntries;
|
||||||
|
@ -253,9 +556,15 @@ export class RDPChannelTree {
|
||||||
return result;
|
return result;
|
||||||
}).filter(e => !!e);
|
}).filter(e => !!e);
|
||||||
|
|
||||||
Object.keys(oldEntryInstances).map(key => oldEntryInstances[key]).forEach(entry => {
|
const removedEntries = Object.keys(oldEntryInstances).map(key => oldEntryInstances[key]);
|
||||||
entry.destroy();
|
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({
|
this.refTree.current?.setState({
|
||||||
tree: this.orderedTree.slice(),
|
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")
|
@EventHandler<ChannelTreeUIEvents>("notify_popout_state")
|
||||||
private handleNotifyPopoutState(event: ChannelTreeUIEvents["notify_popout_state"]) {
|
private handleNotifyPopoutState(event: ChannelTreeUIEvents["notify_popout_state"]) {
|
||||||
this.popoutShown = event.shown;
|
this.popoutShown = event.shown;
|
||||||
|
@ -322,7 +621,6 @@ export abstract class RDPEntry {
|
||||||
const events = this.getEvents();
|
const events = this.getEvents();
|
||||||
|
|
||||||
events.fire("query_unread_state", { treeEntryId: this.entryId });
|
events.fire("query_unread_state", { treeEntryId: this.entryId });
|
||||||
events.fire("query_select_state", { treeEntryId: this.entryId });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUnreadUpdate(value: boolean) {
|
handleUnreadUpdate(value: boolean) {
|
||||||
|
@ -332,7 +630,7 @@ export abstract class RDPEntry {
|
||||||
this.refUnread.current?.forceUpdate();
|
this.refUnread.current?.forceUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSelectUpdate(value: boolean) {
|
setSelected(value: boolean) {
|
||||||
if(this.selected === value) { return; }
|
if(this.selected === value) { return; }
|
||||||
|
|
||||||
this.selected = value;
|
this.selected = value;
|
||||||
|
@ -353,16 +651,52 @@ export abstract class RDPEntry {
|
||||||
return this.renderedInstance = this.doRender();
|
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 doRender() : React.ReactElement;
|
||||||
|
|
||||||
protected abstract renderSelectStateUpdate();
|
protected abstract renderSelectStateUpdate();
|
||||||
protected abstract renderPositionUpdate();
|
protected abstract renderPositionUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RDPChannelDragHint = "none" | "top" | "bottom" | "contain";
|
||||||
export class RDPChannel extends RDPEntry {
|
export class RDPChannel extends RDPEntry {
|
||||||
readonly refIcon = React.createRef<ChannelIconClass>();
|
readonly refIcon = React.createRef<ChannelIconClass>();
|
||||||
readonly refIcons = React.createRef<ChannelIconsRenderer>();
|
readonly refIcons = React.createRef<ChannelIconsRenderer>();
|
||||||
readonly refChannel = React.createRef<RendererChannel>();
|
readonly refChannel = React.createRef<RendererChannel>();
|
||||||
|
readonly refChannelContainer = React.createRef<HTMLDivElement>();
|
||||||
|
|
||||||
/* if uninitialized, undefined */
|
/* if uninitialized, undefined */
|
||||||
info: ChannelEntryInfo;
|
info: ChannelEntryInfo;
|
||||||
|
@ -373,8 +707,12 @@ export class RDPChannel extends RDPEntry {
|
||||||
/* if uninitialized, undefined */
|
/* if uninitialized, undefined */
|
||||||
icons: ChannelIcons;
|
icons: ChannelIcons;
|
||||||
|
|
||||||
|
dragHint: "none" | "top" | "bottom" | "contain";
|
||||||
|
|
||||||
constructor(handle: RDPChannelTree, entryId: number) {
|
constructor(handle: RDPChannelTree, entryId: number) {
|
||||||
super(handle, entryId);
|
super(handle, entryId);
|
||||||
|
|
||||||
|
this.dragHint = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
doRender(): React.ReactElement {
|
doRender(): React.ReactElement {
|
||||||
|
@ -418,6 +756,13 @@ export class RDPChannel extends RDPEntry {
|
||||||
this.info = newInfo;
|
this.info = newInfo;
|
||||||
this.refChannel.current?.forceUpdate();
|
this.refChannel.current?.forceUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setDragHint(hint: RDPChannelDragHint) {
|
||||||
|
if(this.dragHint === hint) { return; }
|
||||||
|
|
||||||
|
this.dragHint = hint;
|
||||||
|
this.refChannel.current?.forceUpdate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RDPClient extends RDPEntry {
|
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() {
|
render() {
|
||||||
const server = this.props.server;
|
const server = this.props.server;
|
||||||
const selected = this.props.server.selected;
|
const selected = this.props.server.selected;
|
||||||
const events = server.getEvents();
|
|
||||||
|
|
||||||
let name, icon;
|
let name, icon;
|
||||||
switch (server.state?.state) {
|
switch (server.state?.state) {
|
||||||
|
@ -39,16 +38,12 @@ export class ServerRenderer extends React.Component<{ server: RDPServer }, {}> {
|
||||||
<div
|
<div
|
||||||
className={serverStyle.serverEntry + " " + viewStyle.treeEntry + " " + (selected ? viewStyle.selected : "")}
|
className={serverStyle.serverEntry + " " + viewStyle.treeEntry + " " + (selected ? viewStyle.selected : "")}
|
||||||
style={{ top: server.offsetTop }}
|
style={{ top: server.offsetTop }}
|
||||||
onMouseUp={event => {
|
onMouseDown={event => {
|
||||||
if (event.button !== 0) {
|
if (event.button !== 0) {
|
||||||
return; /* only left mouse clicks */
|
return; /* only left mouse clicks */
|
||||||
}
|
}
|
||||||
|
|
||||||
events.fire("action_select", {
|
this.props.server.select("auto");
|
||||||
entryIds: [ server.entryId ],
|
|
||||||
mode: "auto",
|
|
||||||
ignoreClientMove: false
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
onContextMenu={event => {
|
onContextMenu={event => {
|
||||||
if (settings.static(Settings.KEY_DISABLE_CONTEXT_MENU)) {
|
if (settings.static(Settings.KEY_DISABLE_CONTEXT_MENU)) {
|
||||||
|
@ -56,8 +51,12 @@ export class ServerRenderer extends React.Component<{ server: RDPServer }, {}> {
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
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" }} />
|
<div className={viewStyle.leftPadding} style={{ paddingLeft: server.offsetLeft + "em" }} />
|
||||||
<UnreadMarkerRenderer entry={server} ref={server.refUnread} />
|
<UnreadMarkerRenderer entry={server} ref={server.refUnread} />
|
||||||
|
|
|
@ -8,7 +8,6 @@ import * as React from "react";
|
||||||
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
|
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
|
||||||
import ResizeObserver from 'resize-observer-polyfill';
|
import ResizeObserver from 'resize-observer-polyfill';
|
||||||
import {RDPChannelTree, RDPEntry} from "./RendererDataProvider";
|
import {RDPChannelTree, RDPEntry} from "./RendererDataProvider";
|
||||||
import {RendererMove} from "./RendererMove";
|
|
||||||
import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
|
import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
|
||||||
import {ClientIcon} from "svg-sprites/client-icons";
|
import {ClientIcon} from "svg-sprites/client-icons";
|
||||||
|
|
||||||
|
@ -59,14 +58,6 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
|
||||||
|
|
||||||
private scrollFixRequested;
|
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: {
|
private inViewCallbacks: {
|
||||||
index: number,
|
index: number,
|
||||||
callback: () => void,
|
callback: () => void,
|
||||||
|
@ -84,16 +75,6 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
|
||||||
tree: [],
|
tree: [],
|
||||||
treeRevision: -1
|
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 {
|
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() {
|
private visibleEntries() {
|
||||||
let viewEntryCount = Math.ceil(this.state.view_height / ChannelTreeView.EntryHeight);
|
let viewEntryCount = Math.ceil(this.state.view_height / ChannelTreeView.EntryHeight);
|
||||||
const viewEntryBegin = Math.floor(this.state.scroll_offset / 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 : "")}
|
className={viewStyle.channelTreeContainer + " " + (this.state.smoothScroll ? viewStyle.smoothScroll : "")}
|
||||||
onScroll={() => this.onScroll()}
|
onScroll={() => this.onScroll()}
|
||||||
ref={this.refContainer}
|
ref={this.refContainer}
|
||||||
onMouseDown={e => this.onMouseDown(e)}
|
|
||||||
onMouseMove={e => this.onMouseMove(e)}
|
|
||||||
onContextMenu={event => {
|
onContextMenu={event => {
|
||||||
if(event.target !== this.refContainer.current) { return; }
|
if(event.target !== this.refContainer.current) { return; }
|
||||||
|
|
||||||
event.preventDefault();
|
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
|
<div
|
||||||
|
@ -204,16 +173,6 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
|
||||||
style={{height: (this.state.tree.length * ChannelTreeView.EntryHeight) + "px"}}>
|
style={{height: (this.state.tree.length * ChannelTreeView.EntryHeight) + "px"}}>
|
||||||
{elements}
|
{elements}
|
||||||
</div>
|
</div>
|
||||||
<RendererMove
|
|
||||||
onMoveEnd={target => {
|
|
||||||
const targetEntry = this.getEntryFromPoint(target.x, target.y);
|
|
||||||
this.props.events.fire("action_move_entries", { treeEntryId: typeof targetEntry === "number" ? targetEntry : 0 });
|
|
||||||
}}
|
|
||||||
onMoveCancel={() => {
|
|
||||||
this.props.events.fire("action_move_entries", { treeEntryId: 0 });
|
|
||||||
}}
|
|
||||||
ref={this.props.dataProvider.refMove}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
scrollEntryInView(entryId: number, callback?: () => void) {
|
||||||
const index = this.state.tree.findIndex(e => e.entryId === entryId);
|
const index = this.state.tree.findIndex(e => e.entryId === entryId);
|
||||||
if (index === -1) {
|
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 => {
|
this.listenerClientMoved = this.rtcConnection.getConnection().command_handler_boss().register_explicit_handler("notifyclientmoved", event => {
|
||||||
const localClientId = this.rtcConnection.getConnection().client.getClientId();
|
const localClientId = this.rtcConnection.getConnection().client.getClientId();
|
||||||
for(const data of event.arguments) {
|
for(const data of event.arguments) {
|
||||||
if(parseInt(data["clid"]) === localClientId) {
|
if(parseInt(data["clid"]) === localClientId) {
|
||||||
/* TODO: Error handling if we failed to start */
|
this.rtcConnection.startTrackBroadcast("audio").catch(error => {
|
||||||
this.rtcConnection.startTrackBroadcast("audio");
|
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…
Add table
Reference in a new issue