Browse Source

Created new message box window and moved console to InABox.Wpf

Kenric Nugteren 1 year ago
parent
commit
6345637b4a

+ 8 - 0
inabox.wpf/Forms/Console/CollapsibleLogEntry.cs

@@ -0,0 +1,8 @@
+using System.Collections.Generic;
+
+namespace InABox.Wpf.Console;
+
+public class CollapsibleLogEntry : LogEntry
+{
+    public List<LogEntry> Contents { get; set; }
+}

+ 156 - 0
inabox.wpf/Forms/Console/Console.xaml

@@ -0,0 +1,156 @@
+<UserControl x:Class="InABox.Wpf.Console.ConsoleControl"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:local="clr-namespace:InABox.Wpf.Console"
+		xmlns:wpf="clr-namespace:InABox.Wpf"
+        mc:Ignorable="d"
+        x:Name="Window"
+        DataContext="{Binding ElementName=Window}">
+    <UserControl.Resources>
+        <wpf:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
+
+        <Style TargetType="ItemsControl" x:Key="LogViewerStyle">
+
+            <Setter Property="Template">
+                <Setter.Value>
+                    <ControlTemplate>
+                        <ScrollViewer CanContentScroll="True">
+                            <ItemsPresenter />
+                        </ScrollViewer>
+                    </ControlTemplate>
+                </Setter.Value>
+            </Setter>
+
+            <Setter Property="ItemsPanel">
+                <Setter.Value>
+                    <ItemsPanelTemplate>
+                        <VirtualizingStackPanel IsItemsHost="True" />
+                    </ItemsPanelTemplate>
+                </Setter.Value>
+            </Setter>
+        </Style>
+
+        <DataTemplate DataType="{x:Type local:LogEntry}">
+            <Grid IsSharedSizeScope="True">
+                <Grid.ColumnDefinitions>
+                    <ColumnDefinition SharedSizeGroup="Date" Width="120" />
+                    <ColumnDefinition SharedSizeGroup="Type" Width="60" />
+                    <ColumnDefinition SharedSizeGroup="User" Width="120" />
+                    <ColumnDefinition />
+                </Grid.ColumnDefinitions>
+
+                <TextBlock Text="{Binding DateTime}" Grid.Column="0" FontFamily="Courier New"
+                           Margin="5,0,5,0" />
+
+                <TextBlock Text="{Binding Type}" Grid.Column="1" FontFamily="Courier New"
+                           Margin="5,0,5,0" />
+
+                <TextBlock Text="{Binding User}" Grid.Column="2" FontFamily="Courier New"
+                           Margin="0,0,2,0" />
+
+                <TextBlock Text="{Binding Message}" Grid.Column="3" FontFamily="Courier New"
+                           TextWrapping="Wrap" />
+            </Grid>
+        </DataTemplate>
+
+        <DataTemplate DataType="{x:Type local:CollapsibleLogEntry}">
+            <Grid IsSharedSizeScope="True">
+                <Grid.ColumnDefinitions>
+                    <ColumnDefinition SharedSizeGroup="Date" Width="120" />
+                    <ColumnDefinition SharedSizeGroup="Type" Width="60" />
+                    <ColumnDefinition SharedSizeGroup="User" Width="120" />
+                    <ColumnDefinition />
+                </Grid.ColumnDefinitions>
+
+                <Grid.RowDefinitions>
+                    <RowDefinition Height="Auto" />
+                    <RowDefinition />
+                </Grid.RowDefinitions>
+
+                <TextBlock Text="{Binding DateTime}" Grid.Column="0" FontFamily="Courier New"
+                           Margin="5,0,5,0" />
+
+                <TextBlock Text="{Binding Type}" Grid.Column="1" FontFamily="Courier New"
+                           Margin="5,0,5,0" />
+
+                <TextBlock Text="{Binding User}" Grid.Column="2" FontFamily="Courier New"
+                           Margin="0,0,2,0" />
+
+                <TextBlock Text="{Binding Message}" Grid.Column="3" FontFamily="Courier New"
+                           TextWrapping="Wrap" />
+
+                <ToggleButton x:Name="Expander" Grid.Row="1" Grid.Column="0"
+                              VerticalAlignment="Top" Content="+" HorizontalAlignment="Right" />
+
+                <ItemsControl ItemsSource="{Binding Contents}" Style="{StaticResource LogViewerStyle}"
+                              Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2"
+                              x:Name="Contents" Visibility="Collapsed" />
+
+            </Grid>
+            <DataTemplate.Triggers>
+                <Trigger SourceName="Expander" Property="IsChecked" Value="True">
+                    <Setter TargetName="Contents" Property="Visibility" Value="Visible" />
+                    <Setter TargetName="Expander" Property="Content" Value="-" />
+                </Trigger>
+            </DataTemplate.Triggers>
+        </DataTemplate>
+
+    </UserControl.Resources>
+    <Grid>
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto" />
+            <RowDefinition Height="*" />
+            <RowDefinition Height="Auto" />
+        </Grid.RowDefinitions>
+        <Grid>
+            <Grid.RowDefinitions>
+                <RowDefinition Height="Auto" />
+            </Grid.RowDefinitions>
+            <Grid.ColumnDefinitions>
+                <ColumnDefinition Width="Auto" />
+                <ColumnDefinition />
+                <ColumnDefinition Width="Auto" />
+                <ColumnDefinition Width="Auto" />
+                <ColumnDefinition Width="Auto" />
+            </Grid.ColumnDefinitions>
+            <Label Content="Search" VerticalContentAlignment="Center" HorizontalAlignment="Left" />
+            <TextBox x:Name="Search" Margin="5,0,0,0" DockPanel.Dock="Right" Background="LightYellow"
+                     VerticalContentAlignment="Center" PreviewKeyDown="Search_KeyDown" TextChanged="Search_TextChanged"
+                     HorizontalAlignment="Stretch" Grid.Column="1" />
+            <CheckBox x:Name="UseRegEx" Content="Use Regular Expressions" HorizontalAlignment="Right"
+                      VerticalAlignment="Center" Visibility="Visible" Margin="5,0,5,0" Grid.Column="2"
+                      Checked="UseRegEx_Checked" Unchecked="UseRegEx_Unchecked" />
+            <CheckBox x:Name="ShowImportant" Content="Show only Important" HorizontalAlignment="Right"
+                      VerticalAlignment="Center" Visibility="Visible" Margin="5,0,5,0" Grid.Column="3"
+                      Checked="ShowImportant_Checked" Unchecked="ShowImportant_Unchecked"/>
+            <Button x:Name="LoadLog" Grid.Column="4" Content="Load Log File" Padding="4,1,4,1" Margin="5,5,5,5"
+                    Click="LoadLog_Click" Visibility="{Binding Path=ShowLoadLogButton,Mode=OneWay,Converter={StaticResource BoolToVisibilityConverter}}" />
+            <Button x:Name="CloseLog" Grid.Column="4" Content="Close Log File" Padding="4,1,4,1" Margin="5,5,5,5"
+                    Click="CloseLog_Click" Visibility="{Binding Path=ShowCloseLogButton,Mode=OneWay,Converter={StaticResource BoolToVisibilityConverter}}" />
+        </Grid>
+        <Border x:Name="LogBorder" BorderBrush="Gray" Grid.Row="1" BorderThickness="0.75" Margin="5" Padding="2">
+            <Border.Style>
+                <Style TargetType="Border">
+                    <Setter Property="Background" Value="LightYellow"/>
+                    <Style.Triggers>
+                        <DataTrigger Binding="{Binding Enabled,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type local:ConsoleControl}}}" Value="False">
+                            <Setter Property="Background" Value="WhiteSmoke"/>
+                        </DataTrigger>
+                    </Style.Triggers>
+                </Style>
+            </Border.Style>
+            <ItemsControl x:Name="Log" DataContext="{Binding Filtered}" ItemsSource="{Binding}" Style="{StaticResource LogViewerStyle}">
+                <ItemsControl.Template>
+                    <ControlTemplate>
+                        <ScrollViewer CanContentScroll="True">
+                            <ItemsPresenter />
+                        </ScrollViewer>
+                    </ControlTemplate>
+                </ItemsControl.Template>
+            </ItemsControl>
+        </Border>
+        <Label x:Name="Error" Content="" Grid.Row="2" HorizontalAlignment="Right" Visibility="Collapsed" />
+    </Grid>
+</UserControl>

+ 374 - 0
inabox.wpf/Forms/Console/Console.xaml.cs

@@ -0,0 +1,374 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Text.RegularExpressions;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Input;
+using System.Windows.Media;
+using com.sun.org.glassfish.gmbal;
+using FastReport.DataVisualization.Charting;
+using Microsoft.Win32;
+
+namespace InABox.Wpf.Console;
+
+public static class ItemsControlExtensions
+{
+    public static void ScrollIntoView(
+        this ItemsControl control,
+        object item)
+    {
+        var framework =
+            control.ItemContainerGenerator.ContainerFromItem(item)
+                as FrameworkElement;
+        if (framework == null) return;
+        framework.BringIntoView();
+    }
+
+    public static void ScrollIntoView(this ItemsControl control)
+    {
+        var count = control.Items.Count;
+        if (count == 0) return;
+        var item = control.Items[count - 1];
+        control.ScrollIntoView(item);
+    }
+}
+
+
+/// <summary>
+///     Interaction logic for Console.xaml
+/// </summary>
+public partial class ConsoleControl : UserControl, INotifyPropertyChanged
+{
+    private CollectionViewSource _filtered;
+    public CollectionViewSource Filtered
+    {
+        get => _filtered;
+        set
+        {
+            _filtered = value;
+            OnPropertyChanged();
+        }
+    }
+
+    public readonly ObservableCollection<LogEntry> LogEntries;
+
+    private readonly TimeSpan regexTimeOut = TimeSpan.FromMilliseconds(100);
+
+    private Regex? searchRegex;
+
+    private bool _enabled = true;
+
+    public event PropertyChangedEventHandler? PropertyChanged;
+
+    public event Action? OnLoadLog;
+    public event Action? OnCloseLog;
+
+    public bool _allowLoadLogButton = true;
+    public bool AllowLoadLogButton
+    {
+        get => _allowLoadLogButton;
+        set
+        {
+            _allowLoadLogButton = value;
+            OnPropertyChanged();
+            OnPropertyChanged(nameof(ShowLoadLogButton));
+            OnPropertyChanged(nameof(ShowCloseLogButton));
+        }
+    }
+
+    private bool _loadedLog = false;
+    public bool LoadedLog
+    {
+        get => _loadedLog;
+        set
+        {
+            _loadedLog = value;
+            OnPropertyChanged();
+            OnPropertyChanged(nameof(ShowLoadLogButton));
+            OnPropertyChanged(nameof(ShowCloseLogButton));
+        }
+    }
+
+    public bool ShowLoadLogButton => !LoadedLog && AllowLoadLogButton;
+    public bool ShowCloseLogButton => LoadedLog && AllowLoadLogButton;
+
+    public bool Enabled
+    {
+        get => _enabled;
+        set
+        {
+            _enabled = value;
+            OnPropertyChanged();
+        }
+    }
+
+    public ConsoleControl()
+    {
+        InitializeComponent();
+
+        Filtered = new CollectionViewSource();
+        LogEntries = new ObservableCollection<LogEntry>();
+        Filtered.Source = LogEntries;
+        Filtered.Filter += (sender, args) =>
+        {
+            var logEntry = (LogEntry)args.Item;
+            if (ShowImportant.IsChecked == true && !IsImportant(logEntry))
+            {
+                args.Accepted = false;
+                return;
+            }
+
+            if (UseRegEx.IsChecked == true && searchRegex != null)
+                args.Accepted = string.IsNullOrWhiteSpace(Search.Text)
+                                || searchRegex.IsMatch(logEntry.DateTime)
+                                || searchRegex.IsMatch(logEntry.Type)
+                                || searchRegex.IsMatch(logEntry.User)
+                                || searchRegex.IsMatch(logEntry.Message);
+            else
+                args.Accepted = string.IsNullOrWhiteSpace(Search.Text)
+                                || logEntry.DateTime.Contains(Search.Text)
+                                || logEntry.Type.Contains(Search.Text)
+                                || logEntry.User.Contains(Search.Text)
+                                || logEntry.Message.Contains(Search.Text);
+        };
+    }
+
+    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
+    {
+        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+    }
+
+    private static bool IsImportant(LogEntry entry)
+    {
+        return entry.Type == "IMPTNT";
+    }
+
+    public static LogEntry ParseLogMessage(string logLine)
+    {
+        var datetime = logLine.Length > 32 ? logLine[..12] : string.Format("{0:HH:mm:ss.fff}", DateTime.Now);
+        var type = logLine.Length > 32 ? logLine.Substring(13, 6).Trim() : "INFO";
+        var user = logLine.Length > 32 ? logLine.Substring(20, 12) : "";
+        var msg = logLine.Length > 32 ? logLine[33..] : logLine;
+
+        return new LogEntry
+        {
+            DateTime = datetime,
+            Type = type,
+            User = user,
+            Message = msg
+        };
+    }
+
+    public void LoadLogEntries(IEnumerable<string> lines)
+    {
+        var logEntries = new List<LogEntry>();
+
+        var numberSkipped = 0;
+
+        foreach (var line in lines)
+        {
+            var logEntry = ParseLogMessage(line ?? "");
+            var logType = logEntry.Type;
+            if (logType == "ERROR" || logType == "INFO" || logType == "IMPTNT")
+                logEntries.Add(logEntry);
+            else if (string.IsNullOrWhiteSpace(logType)) numberSkipped++;
+        }
+
+        if (numberSkipped > 0)
+        {
+            if (logEntries.Count == 0)
+                SetErrorMessage("File does not contain valid log information!");
+            else
+                SetErrorMessage(string.Format("Skipped {0} lines that did not contain valid log information", numberSkipped));
+        }
+
+        Filtered.Source = logEntries;
+    }
+    public void LoadLogEntry(string line)
+    {
+        var logEntry = ParseLogMessage(line);
+        var logType = logEntry.Type;
+        if (logType == "INFO" || logType == "ERROR" || logType == "IMPTNT")
+        {
+            LogEntries.Insert(0, logEntry);
+        }
+    }
+
+
+    private void Search_KeyDown(object sender, KeyEventArgs e)
+    {
+        if (e.Key == Key.Enter && UseRegEx.IsChecked == true)
+        {
+            try
+            {
+                searchRegex = new Regex(Search.Text, RegexOptions.Compiled, regexTimeOut);
+            }
+            catch (ArgumentException)
+            {
+                searchRegex = null;
+            }
+
+            Filtered.View.Refresh();
+            SetSearchStyleNormal();
+        }
+    }
+
+    private void SetSearchStyleChanged()
+    {
+        Search.Background = Brushes.White;
+    }
+
+    private void SetSearchStyleNormal()
+    {
+        Search.Background = Brushes.LightYellow;
+    }
+
+    private void Search_TextChanged(object sender, TextChangedEventArgs e)
+    {
+        if (UseRegEx.IsChecked != true)
+        {
+            Filtered.View.Refresh();
+        }
+        else
+        {
+            if (string.IsNullOrWhiteSpace(Search.Text))
+            {
+                searchRegex = null;
+                Filtered.View.Refresh();
+                SetSearchStyleNormal();
+            }
+            else
+            {
+                SetSearchStyleChanged();
+            }
+        }
+    }
+
+    private void UseRegEx_Checked(object sender, RoutedEventArgs e)
+    {
+        try
+        {
+            searchRegex = new Regex(Search.Text, RegexOptions.Compiled, regexTimeOut);
+        }
+        catch (ArgumentException ex)
+        {
+            searchRegex = null;
+        }
+
+        Filtered.View.Refresh();
+    }
+
+    private void UseRegEx_Unchecked(object sender, RoutedEventArgs e)
+    {
+        searchRegex = null;
+        Filtered.View.Refresh();
+
+        SetSearchStyleNormal();
+    }
+
+    public void SetErrorMessage(string? error)
+    {
+        if (string.IsNullOrWhiteSpace(error))
+        {
+            Error.Content = "";
+            Error.Visibility = Visibility.Collapsed;
+        }
+        else
+        {
+            Error.Content = error;
+            Error.Visibility = Visibility.Visible;
+        }
+    }
+
+    private void CloseLog_Click(object sender, RoutedEventArgs e)
+    {
+        Filtered.Source = LogEntries;
+
+        OnCloseLog?.Invoke();
+    }
+
+    private void ShowImportant_Checked(object sender, RoutedEventArgs e)
+    {
+        Filtered.View.Refresh();
+    }
+
+    private void ShowImportant_Unchecked(object sender, RoutedEventArgs e)
+    {
+        Filtered.View.Refresh();
+    }
+
+    private void LoadLog_Click(object sender, RoutedEventArgs e)
+    {
+        OnLoadLog?.Invoke();
+    }
+}
+
+public abstract class Console : Window
+{
+    public ConsoleControl ConsoleControl { get; set; }
+
+    private readonly string Description;
+
+    public Console(string description)
+    {
+        ConsoleControl = new ConsoleControl();
+        ConsoleControl.OnLoadLog += ConsoleControl_OnLoadLog;
+        ConsoleControl.OnCloseLog += ConsoleControl_OnCloseLog;
+        Content = ConsoleControl;
+
+        Height = 800;
+        Width = 1200;
+
+        Loaded += Console_Loaded;
+        Closing += Console_Closing;
+
+        Title = description;
+        Description = description;
+    }
+
+    private void ConsoleControl_OnCloseLog()
+    {
+        Title = Description;
+        ConsoleControl.LoadedLog = false;
+    }
+
+    private void ConsoleControl_OnLoadLog()
+    {
+        var dialog = new OpenFileDialog
+        {
+            InitialDirectory = GetLogDirectory()
+        };
+        if (dialog.ShowDialog() == true)
+        {
+            var lines = File.ReadLines(dialog.FileName);
+            ConsoleControl.LoadLogEntries(lines);
+            ConsoleControl.LoadedLog = true;
+
+            Title = dialog.FileName;
+        }
+    }
+
+    protected virtual void OnLoaded()
+    {
+    }
+    protected virtual void OnClosing()
+    {
+    }
+
+    private void Console_Closing(object? sender, CancelEventArgs e)
+    {
+        OnClosing();
+    }
+
+    private void Console_Loaded(object sender, RoutedEventArgs e)
+    {
+        OnLoaded();
+    }
+
+    protected abstract string GetLogDirectory();
+}

+ 12 - 0
inabox.wpf/Forms/Console/LogEntry.cs

@@ -0,0 +1,12 @@
+namespace InABox.Wpf.Console;
+
+public class LogEntry : PropertyChangedBase
+{
+    public string DateTime { get; set; }
+
+    public string Type { get; set; }
+
+    public string User { get; set; }
+
+    public string Message { get; set; }
+}

+ 20 - 0
inabox.wpf/Forms/Console/PropertyChangedBase.cs

@@ -0,0 +1,20 @@
+using System;
+using System.ComponentModel;
+using System.Windows;
+
+namespace InABox.Wpf.Console;
+
+public class PropertyChangedBase : INotifyPropertyChanged
+{
+    public event PropertyChangedEventHandler PropertyChanged;
+
+    protected virtual void OnPropertyChanged(string propertyName)
+    {
+        Application.Current.Dispatcher.BeginInvoke((Action)(() =>
+        {
+            var handler = PropertyChanged;
+            if (handler != null)
+                handler(this, new PropertyChangedEventArgs(propertyName));
+        }));
+    }
+}

+ 85 - 0
inabox.wpf/Forms/MessageWindow.xaml

@@ -0,0 +1,85 @@
+<Window x:Class="InABox.Wpf.MessageWindow"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:local="clr-namespace:InABox.Wpf"
+        mc:Ignorable="d"
+        Width="280"
+        SizeToContent="Height"
+        x:Name="Window"
+        DataContext="{Binding ElementName=Window}">
+    <Window.Resources>
+        <DataTemplate x:Key="ButtonTemplate" DataType="local:MessageWindowButton">
+            <Button Content="{Binding Content}"
+                    MinWidth="75" Padding="5" Margin="5"
+                    Click="Button_Click"
+                    Tag="{Binding}">
+
+            </Button>
+        </DataTemplate>
+    </Window.Resources>
+    <Grid>
+        <Grid.RowDefinitions>
+            <RowDefinition Height="*" MinHeight="100"/>
+            <RowDefinition Height="Auto"/>
+            <RowDefinition MaxHeight="150">
+                <RowDefinition.Style>
+                    <Style TargetType="RowDefinition">
+                        <Setter Property="Height" Value="0"/>
+                        <Style.Triggers>
+                            <DataTrigger Binding="{Binding ShowDetails}" Value="True">
+                                <Setter Property="Height" Value="*"/>
+                            </DataTrigger>
+                        </Style.Triggers>
+                    </Style>
+                </RowDefinition.Style>
+            </RowDefinition>
+        </Grid.RowDefinitions>
+        <Border Grid.Row="0" BorderBrush="LightGray" BorderThickness="1"
+                Margin="2" Padding="5" Background="White">
+            <TextBlock x:Name="MessageBox" Text="{Binding Message}"
+                       TextWrapping="Wrap"
+                       Margin="5" VerticalAlignment="Center"/>
+        </Border>
+        <Border Grid.Row="1"
+                Background="WhiteSmoke" BorderBrush="Gray" BorderThickness="1" Margin="2,0,2,2">
+            <Grid>
+                <Grid.ColumnDefinitions>
+                    <ColumnDefinition Width="Auto"/>
+                    <ColumnDefinition Width="*"/>
+                    <ColumnDefinition Width="Auto"/>
+                </Grid.ColumnDefinitions>
+                <ItemsControl ItemsSource="{Binding LeftButtons}"
+                              Grid.Column="0"
+                              ItemTemplate="{StaticResource ButtonTemplate}">
+                    <ItemsControl.ItemsPanel>
+                        <ItemsPanelTemplate>
+                            <StackPanel Orientation="Horizontal"/>
+                        </ItemsPanelTemplate>
+                    </ItemsControl.ItemsPanel>
+                </ItemsControl>
+                <ItemsControl ItemsSource="{Binding RightButtons}"
+                              Grid.Column="2"
+                              ItemTemplate="{StaticResource ButtonTemplate}">
+                    <ItemsControl.ItemsPanel>
+                        <ItemsPanelTemplate>
+                            <StackPanel Orientation="Horizontal"/>
+                        </ItemsPanelTemplate>
+                    </ItemsControl.ItemsPanel>
+                </ItemsControl>
+            </Grid>
+        </Border>
+        <Border Grid.Row="2" BorderBrush="LightGray" BorderThickness="1"
+                Margin="2" Padding="2" Background="White">
+            <ScrollViewer VerticalScrollBarVisibility="Auto">
+                <Border BorderBrush="LightGray" BorderThickness="1"
+                        Background="WhiteSmoke">
+                    <TextBlock x:Name="DetailsBox" Text="{Binding Details}"
+                               TextWrapping="Wrap"
+                               Padding="5" VerticalAlignment="Center"/>
+                </Border>
+            </ScrollViewer>
+        </Border>
+    </Grid>
+</Window>

+ 273 - 0
inabox.wpf/Forms/MessageWindow.xaml.cs

@@ -0,0 +1,273 @@
+using InABox.Clients;
+using InABox.Core;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Windows;
+using System.Windows.Controls;
+using System.IO;
+
+namespace InABox.Wpf;
+
+public enum MessageWindowButtonPosition
+{
+    Left,
+    Right
+}
+
+public enum MessageWindowResult
+{
+    None,
+    OK,
+    Cancel,
+    Other
+}
+
+public class MessageWindowButton : INotifyPropertyChanged
+{
+    public delegate void MessageWindowButtonDelegate(MessageWindow window, MessageWindowButton button);
+
+    public MessageWindowButtonPosition Position { get; set; }
+
+    private string _content;
+    public string Content
+    {
+        get => _content;
+        [MemberNotNull(nameof(_content))]
+        set
+        {
+            _content = value;
+            OnPropertyChanged();
+        }
+    }
+
+    public MessageWindowButtonDelegate Action { get; set; }
+
+    public MessageWindowButton(string content, MessageWindowButtonDelegate action, MessageWindowButtonPosition position)
+    {
+        Content = content;
+        Action = action;
+        Position = position;
+    }
+
+    public event PropertyChangedEventHandler? PropertyChanged;
+
+    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
+    {
+        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+    }
+}
+
+/// <summary>
+/// Interaction logic for MessageWindow.xaml
+/// </summary>
+public partial class MessageWindow : Window, INotifyPropertyChanged
+{
+    public ObservableCollection<MessageWindowButton> Buttons { get; private set; } = new();
+
+    public IEnumerable<MessageWindowButton> LeftButtons => Buttons.Where(x => x.Position == MessageWindowButtonPosition.Left);
+    public IEnumerable<MessageWindowButton> RightButtons => Buttons.Where(x => x.Position == MessageWindowButtonPosition.Right);
+
+    private string _message = "";
+    public string Message
+    {
+        get => _message;
+        set
+        {
+            _message = value;
+            OnPropertyChanged();
+        }
+    }
+
+    private string _details = "";
+
+    public string Details
+    {
+        get => _details;
+        set
+        {
+            _details = value;
+            OnPropertyChanged();
+        }
+    }
+
+    public static readonly DependencyProperty ShowDetailsProperty = DependencyProperty.Register(nameof(ShowDetails), typeof(bool), typeof(MessageWindow));
+
+    public bool ShowDetails
+    {
+        get => (bool)GetValue(ShowDetailsProperty);
+        set => SetValue(ShowDetailsProperty, value);
+    }
+
+    public MessageWindowResult Result { get; set; } = MessageWindowResult.None;
+
+    public object? OtherResult { get; set; }
+
+    public MessageWindow()
+    {
+        InitializeComponent();
+
+        Buttons.CollectionChanged += Buttons_CollectionChanged;
+    }
+
+    private void Button_Click(object sender, RoutedEventArgs e)
+    {
+        if (sender is not Button button || button.Tag is not MessageWindowButton winButton) return;
+
+        winButton.Action(this, winButton);
+    }
+
+    private void Buttons_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
+    {
+        OnPropertyChanged(nameof(LeftButtons));
+        OnPropertyChanged(nameof(RightButtons));
+    }
+
+    public void AddButton(MessageWindowButton button)
+    {
+        Buttons.Add(button);
+    }
+
+    public void AddOKButton(string content = "OK")
+    {
+        Buttons.Add(new MessageWindowButton(content, OKButton_Click, MessageWindowButtonPosition.Right));
+    }
+
+    public void AddCancelButton(string content = "Cancel")
+    {
+        Buttons.Add(new MessageWindowButton(content, CancelButton_Click, MessageWindowButtonPosition.Right));
+    }
+
+    private void CancelButton_Click(MessageWindow window, MessageWindowButton button)
+    {
+        Result = MessageWindowResult.Cancel;
+        Close();
+    }
+
+    private void OKButton_Click(MessageWindow window, MessageWindowButton button)
+    {
+        Result = MessageWindowResult.OK;
+        Close();
+    }
+
+    public event PropertyChangedEventHandler? PropertyChanged;
+
+    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
+    {
+        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+    }
+
+    #region Static Constructors
+
+    public static void ShowMessage(string message, string title)
+    {
+        var window = new MessageWindow
+        {
+            Message = message,
+            Title = title
+        };
+        window.AddOKButton();
+        window.ShowDialog();
+    }
+
+    public static bool ShowOKCancel(string message, string title)
+    {
+        var window = new MessageWindow
+        {
+            Message = message,
+            Title = title
+        };
+        window.AddOKButton();
+        window.AddCancelButton();
+        window.ShowDialog();
+        return window.Result == MessageWindowResult.OK;
+    }
+
+    public static bool ShowYesNo(string message, string title)
+    {
+        var window = new MessageWindow
+        {
+            Message = message,
+            Title = title
+        };
+        window.AddOKButton("Yes");
+        window.AddCancelButton("No");
+        window.ShowDialog();
+        return window.Result == MessageWindowResult.OK;
+    }
+
+    /// <summary>
+    /// Display a message box for an exception, giving options to view the logs.
+    /// </summary>
+    /// <param name="message">The message to display. Set to <see langword="null"/> to default to the exception message.</param>
+    /// <param name="exception"></param>
+    /// <param name="title"></param>
+    /// <param name="shouldLog">If <see langword="true"/>, also logs the exception.</param>
+    public static void ShowError(string? message, Exception exception, string title = "Error", bool shouldLog = true)
+    {
+        if (shouldLog)
+        {
+            CoreUtils.LogException(ClientFactory.UserID, exception);
+        }
+
+        var window = new MessageWindow
+        {
+            Message = message ?? exception.Message,
+            Title = title,
+            Details = CoreUtils.FormatException(exception)
+        };
+
+        window.AddButton(new MessageWindowButton("Show Logs", ShowLogs_Click, MessageWindowButtonPosition.Left));
+
+        var showDetailsButton = new MessageWindowButton("Show Details", (win, button) =>
+        {
+            win.ShowDetails = !win.ShowDetails;
+            button.Content = win.ShowDetails
+                ? "Hide Details"
+                : "Show Details";
+        }, MessageWindowButtonPosition.Left);
+        window.AddButton(showDetailsButton);
+
+        window.AddOKButton();
+        window.ShowDialog();
+    }
+
+    private static void ShowLogs_Click(MessageWindow window, MessageWindowButton button)
+    {
+        var console = new MessageWindowConsole("Logs", Path.Combine(CoreUtils.GetPath(), string.Format("{0:yyyy-MM-dd}.log", DateTime.Today)));
+        console.ShowDialog();
+    }
+
+    #endregion
+}
+
+public class MessageWindowConsole : Console.Console
+{
+    public string FileName { get; set; }
+
+    public MessageWindowConsole(string description, string file) : base(description)
+    {
+        FileName = file;
+        ConsoleControl.AllowLoadLogButton = false;
+    }
+
+    protected override void OnLoaded()
+    {
+        base.OnLoaded();
+
+        if (File.Exists(FileName))
+        {
+            var lines = File.ReadLines(FileName);
+            ConsoleControl.LoadLogEntries(lines);
+        }
+    }
+
+    protected override string GetLogDirectory()
+    {
+        return CoreUtils.GetPath();
+    }
+}