using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Net; using System.Runtime.CompilerServices; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using WpfWindowPlacement; using XIVChatCommon.Message; using XIVChatCommon.Message.Server; namespace XIVChat_Desktop { /// /// Interaction logic for MainWindow.xaml /// public partial class MainWindow : INotifyPropertyChanged { #region commands private void AlwaysTrue_CanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = true; } public static readonly RoutedUICommand EditTab = new RoutedUICommand( "EditTab", "EditTab", typeof(MainWindow) ); private void EditTab_OnExecuted(object sender, ExecutedRoutedEventArgs e) { if (!(e.Parameter is Tab tab)) { return; } new ManageTab(this, tab).Show(); } public static readonly RoutedUICommand DeleteTab = new RoutedUICommand( "DeleteTab", "DeleteTab", typeof(MainWindow) ); private void DeleteTab_OnExecuted(object sender, ExecutedRoutedEventArgs e) { if (!(e.Parameter is Tab tab)) { return; } this.App.Config.Tabs.Remove(tab); this.App.Config.Save(); } public static readonly RoutedUICommand AddTab = new RoutedUICommand( "AddTab", "AddTab", typeof(MainWindow) ); private void AddTab_OnExecuted(object sender, ExecutedRoutedEventArgs e) { new ManageTab(this, null).Show(); } public static readonly RoutedUICommand ManageTabs = new RoutedUICommand( "ManageTabs", "ManageTabs", typeof(MainWindow) ); private void ManageTabs_OnExecuted(object sender, ExecutedRoutedEventArgs e) { new ManageTabs(this).Show(); } public static readonly RoutedUICommand MessageSendTell = new RoutedUICommand( "MessageSendTell", "MessageSendTell", typeof(MainWindow) ); private void MessageSendTell_CanExecute(object sender, CanExecuteRoutedEventArgs e) { if (!(e.Parameter is ServerMessage message)) { return; } e.CanExecute = message.GetSenderPlayer() != null; } private void MessageSendTell_OnExecuted(object eventSender, ExecutedRoutedEventArgs e) { if (!(e.Parameter is ServerMessage message)) { return; } var sender = message.GetSenderPlayer(); if (sender == null) { return; } var worldName = Util.WorldName(sender.Server); if (worldName == null) { return; } this.InsertTellCommand(sender.Name, worldName); } public static readonly RoutedUICommand ChangeChannel = new RoutedUICommand( "ChangeChannel", "ChangeChannel", typeof(MainWindow) ); private void ChangeChannel_CanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = this.App.Connected; } private void ChangeChannel_Execute(object sender, ExecutedRoutedEventArgs e) { if (!(e.Parameter is InputChannel)) { return; } var param = (InputChannel) e.Parameter; this.App.Connection?.ChangeChannel(param); } public static readonly RoutedUICommand OpenLink = new( "OpenLink", "OpenLink", typeof(MainWindow) ); private void OpenLink_CanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = true; } private void OpenLink_Execute(object sender, ExecutedRoutedEventArgs e) { if (!(e.Parameter is string param)) { return; } try { var uri = new Uri(param); if (uri.Scheme == "http" || uri.Scheme == "https") { Process.Start(new ProcessStartInfo { FileName = uri.ToString(), UseShellExecute = true, }); } } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } #endregion public App App => (App) Application.Current; public List Messages { get; } = new List(); public ObservableCollection FriendList { get; } = new ObservableCollection(); private int historyIndex = -1; private int HistoryIndex { get => this.historyIndex; set { var idx = Math.Min(this.History.Count - 1, Math.Max(-1, value)); this.historyIndex = idx; } } private int ReverseHistoryIndex => this.HistoryIndex == -1 ? -1 : Math.Max(-1, this.History.Count - this.HistoryIndex - 1); private string? HistoryBuffer { get; set; } private List History { get; } = new List(); public string InputPlaceholder => this.App.Connection?.Available == true ? "Send a message…" : "Chat is currently unavailable"; public MainWindow() { this.InitializeComponent(); this.DataContext = this; } private T? FindElementByName(DependencyObject element, string sChildName) where T : FrameworkElement { T? childElement = null; var nChildCount = VisualTreeHelper.GetChildrenCount(element); for (var i = 0; i < nChildCount; i++) { if (!(VisualTreeHelper.GetChild(element, i) is FrameworkElement child)) { continue; } if (child is T t && child.Name.Equals(sChildName)) { childElement = t; break; } childElement = this.FindElementByName(child, sChildName); if (childElement != null) { break; } } return childElement; } public void ClearAllMessages() { this.Messages.Clear(); foreach (var tab in this.App.Config.Tabs) { tab.ClearMessages(); } } public void AddSystemMessage(string content) { var message = new ServerMessage( DateTime.UtcNow, 0, new byte[0], Encoding.UTF8.GetBytes(content), new List { new TextChunk(content) { Foreground = 0xb38cffff, }, } ); this.AddMessage(message); } private int lastSequence = -1; private int insertAt; public void AddReversedChunk(ServerMessage[] messages, int sequence) { if (sequence != this.lastSequence) { this.lastSequence = sequence; this.insertAt = this.Messages.Count; } // detect if scroller is at the bottom var scroller = this.FindElementByName(this.Tabs, "scroller"); var scrollerOffset = scroller!.VerticalOffset; var scrollerHeight = scroller.ScrollableHeight; var wasAtBottom = Math.Abs(scroller.VerticalOffset - scrollerHeight) < .0001; // add messages to main list this.Messages.InsertRange(this.insertAt, messages); // add message to each tab if the filter allows for it foreach (var tab in this.App.Config.Tabs) { tab.AddReversedChunk(messages, sequence, this.App.Config); } var diff = this.Messages.Count - this.App.Config.LocalBacklogMessages; if (diff > 0) { this.Messages.RemoveRange(0, (int) diff); } // scroll to the bottom if previously at the bottom if (wasAtBottom) { scroller.ScrollToBottom(); } else { scroller.UpdateLayout(); var scrollDiff = scroller.ScrollableHeight - scrollerHeight; scroller.ScrollToVerticalOffset(scrollerOffset + scrollDiff); } } public void AddMessage(ServerMessage message) { // detect if scroller is at the bottom var scroller = this.FindElementByName(this.Tabs, "scroller"); var verticalOffset = scroller!.VerticalOffset; var wasAtBottom = Math.Abs(verticalOffset - scroller.ScrollableHeight) < .0001; // add message to main list this.Messages.Add(message); // add message to each tab if the filter allows for it foreach (var tab in this.App.Config.Tabs) { tab.AddMessage(message, this.App.Config); } var diff = this.Messages.Count - this.App.Config.LocalBacklogMessages; if (diff > 0) { this.Messages.RemoveRange(0, (int) diff); } // scroll to the bottom if previously at the bottom if (wasAtBottom) { scroller.ScrollToBottom(); } else { scroller.ScrollToVerticalOffset(verticalOffset); } } public void InsertTellCommand(string name, string world, bool focus = true) { var input = this.App.Window.GetCurrentInputBox(); if (input == null) { return; } var tell = $"/tell {name}@{world} "; input.Text = input.Text.Insert(0, tell); input.SelectionStart = tell.Length; input.SelectionLength = input.Text.Length - tell.Length; if (focus) { input.Focus(); } } private void Connect_Click(object sender, RoutedEventArgs e) { new ConnectDialog(this).ShowDialog(); } private void Disconnect_Click(object sender, RoutedEventArgs e) { this.App.Disconnect(); } private void Input_Submit(object sender, KeyEventArgs e) { if (!(sender is TextBox textBox)) { return; } switch (e.Key) { case Key.Return: this.Submit(textBox); break; case Key.Up: this.ArrowNavigate(textBox, true); break; case Key.Down: this.ArrowNavigate(textBox, false); break; } } private void Submit(TextBox textBox) { var conn = this.App.Connection; if (conn == null) { return; } conn.SendMessage(textBox.Text); this.History.Add(textBox.Text); while (this.History.Count > 100) { this.History.RemoveAt(0); } textBox.Text = ""; } private void ArrowNavigate(TextBox textBox, bool up) { if (this.History.Count == 0) { return; } var caretLine = textBox.GetLineIndexFromCharacterIndex(textBox.CaretIndex); var inFirstLine = caretLine == 0; var inLastLine = caretLine == textBox.LineCount - 1; if (this.HistoryIndex == -1) { this.HistoryBuffer = textBox.Text; } if (up && inFirstLine) { // go up in history this.HistoryIndex += 1; textBox.Text = this.History[this.ReverseHistoryIndex]; } else if (!up && inLastLine) { // go down in history this.HistoryIndex -= 1; if (this.HistoryIndex == -1) { textBox.Text = this.HistoryBuffer; this.HistoryBuffer = null; } else { textBox.Text = this.History[this.ReverseHistoryIndex]; } } } private void Configuration_Click(object sender, RoutedEventArgs e) { new ConfigWindow(this, this.App.Config).Show(); } private void Tabs_Loaded(object sender, RoutedEventArgs e) { this.Tabs.SelectedIndex = 0; } public TextBox? GetCurrentInputBox() { return this.FindElementByName(this.Tabs, "InputBox"); } private void Tabs_SelectionChanged(object sender, SelectionChangedEventArgs e) { var scroller = this.FindElementByName(this.Tabs, "scroller"); scroller?.ScrollToBottom(); } private void Scan_Click(object sender, RoutedEventArgs e) { new ServerScan(this).Show(); } public event PropertyChangedEventHandler? PropertyChanged; internal void OnPropertyChanged([CallerMemberName] string? propertyName = null) { this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } private void Export_Click(object sender, RoutedEventArgs e) { new Export(this).Show(); } private void Licence_Click(object sender, RoutedEventArgs e) { new LicenceWindow(this, false).Show(); } private void Exit_Click(object sender, RoutedEventArgs e) { this.Close(); this.App.Shutdown(); } private void FriendList_Click(object sender, RoutedEventArgs e) { new FriendList(this).Show(); } private void Channel_MouseDown(object sender, MouseButtonEventArgs e) { e.Handled = true; if (e.ChangedButton != MouseButton.Left) { return; } var channel = (TextBlock) sender; channel.ContextMenu!.PlacementTarget = channel; channel.ContextMenu!.IsOpen = true; } private void MainWindow_OnSourceInitialized(object? sender, EventArgs e) { this.SetPlacement(this.App.Config.WindowPlacement); } private void MainWindow_OnClosing(object sender, CancelEventArgs e) { this.App.Config.WindowPlacement = this.GetPlacement(); this.App.Config.Save(); } } }