using MainShell.Resources.CustomControl; using Newtonsoft.Json; using Stylet; using System; using System.Globalization; using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; namespace MainShell.Common { public static class HeaderDeviceStatusOverlayHost { private static readonly object OverlayRootTag = new object(); private static readonly HeaderDeviceStatusOverlaySettings OverlaySettings = new HeaderDeviceStatusOverlaySettings(); public static void Attach(Window window, HeaderDeviceStatusController controller) { if (window == null) { throw new ArgumentNullException(nameof(window)); } if (controller == null) { throw new ArgumentNullException(nameof(controller)); } window.Dispatcher.BeginInvoke( DispatcherPriority.Loaded, new Action(() => AttachCore(window, controller))); } private static void AttachCore(Window window, HeaderDeviceStatusController controller) { Grid existingOverlayRoot = window.Content as Grid; if (existingOverlayRoot != null && ReferenceEquals(existingOverlayRoot.Tag, OverlayRootTag)) { return; } UIElement currentContent = window.Content as UIElement; if (currentContent == null) { return; } Grid overlayRoot = new Grid(); overlayRoot.Tag = OverlayRootTag; window.Content = null; overlayRoot.Children.Add(currentContent); overlayRoot.Children.Add(CreateOverlayElement(controller)); overlayRoot.Children.Add(CreateDebugPanelElement()); window.Content = overlayRoot; } private static FrameworkElement CreateOverlayElement(HeaderDeviceStatusController controller) { HeaderDeviceStatusView statusView = new HeaderDeviceStatusView(); statusView.DataContext = controller; statusView.IsHitTestVisible = false; statusView.HorizontalAlignment = HorizontalAlignment.Left; statusView.VerticalAlignment = VerticalAlignment.Top; Binding marginBinding = new Binding(nameof(HeaderDeviceStatusOverlaySettings.Margin)); marginBinding.Source = OverlaySettings; BindingOperations.SetBinding(statusView, FrameworkElement.MarginProperty, marginBinding); Panel.SetZIndex(statusView, short.MaxValue); return statusView; } private static FrameworkElement CreateDebugPanelElement() { Border panelBorder = new Border(); panelBorder.Background = new SolidColorBrush(Color.FromArgb(235, 18, 26, 35)); panelBorder.BorderBrush = new SolidColorBrush(Color.FromRgb(72, 93, 118)); panelBorder.BorderThickness = new Thickness(1.0); panelBorder.CornerRadius = new CornerRadius(6.0); panelBorder.Padding = new Thickness(10.0, 8.0, 10.0, 8.0); panelBorder.HorizontalAlignment = HorizontalAlignment.Left; panelBorder.VerticalAlignment = VerticalAlignment.Top; Binding panelMarginBinding = new Binding(nameof(HeaderDeviceStatusOverlaySettings.DebugPanelMargin)); panelMarginBinding.Source = OverlaySettings; BindingOperations.SetBinding(panelBorder, FrameworkElement.MarginProperty, panelMarginBinding); Binding panelVisibilityBinding = new Binding(nameof(HeaderDeviceStatusOverlaySettings.IsDebugPanelVisible)); panelVisibilityBinding.Source = OverlaySettings; panelVisibilityBinding.Converter = new BooleanToVisibilityConverter(); BindingOperations.SetBinding(panelBorder, UIElement.VisibilityProperty, panelVisibilityBinding); StackPanel rootPanel = new StackPanel(); rootPanel.Orientation = Orientation.Vertical; DockPanel headerPanel = new DockPanel(); TextBlock titleText = new TextBlock(); titleText.Text = "Overlay Debug"; titleText.Foreground = Brushes.White; titleText.FontSize = 13.0; titleText.FontWeight = FontWeights.SemiBold; titleText.Margin = new Thickness(0.0, 0.0, 8.0, 6.0); DockPanel.SetDock(titleText, Dock.Left); headerPanel.Children.Add(titleText); Button closeButton = CreateDebugButton("隐藏", 44.0); closeButton.Margin = new Thickness(6.0, 0.0, 0.0, 6.0); closeButton.Click += delegate(object sender, RoutedEventArgs e) { OverlaySettings.HideDebugPanel(); }; DockPanel.SetDock(closeButton, Dock.Right); headerPanel.Children.Add(closeButton); rootPanel.Children.Add(headerPanel); TextBlock summaryText = new TextBlock(); summaryText.Foreground = new SolidColorBrush(Color.FromRgb(210, 221, 236)); summaryText.FontSize = 12.0; summaryText.Margin = new Thickness(0.0, 0.0, 0.0, 8.0); Binding summaryBinding = new Binding(nameof(HeaderDeviceStatusOverlaySettings.PositionSummary)); summaryBinding.Source = OverlaySettings; BindingOperations.SetBinding(summaryText, TextBlock.TextProperty, summaryBinding); rootPanel.Children.Add(summaryText); rootPanel.Children.Add(CreateDebugInputRow("Left", nameof(HeaderDeviceStatusOverlaySettings.LeftMarginText))); rootPanel.Children.Add(CreateDebugInputRow("Top", nameof(HeaderDeviceStatusOverlaySettings.TopMarginText))); rootPanel.Children.Add(CreateDebugInputRow("Step", nameof(HeaderDeviceStatusOverlaySettings.StepText))); UniformGrid actionGrid = new UniformGrid(); actionGrid.Columns = 4; actionGrid.Rows = 1; actionGrid.Margin = new Thickness(0.0, 0.0, 0.0, 8.0); Button leftButton = CreateDebugButton("左", 40.0); leftButton.Click += delegate(object sender, RoutedEventArgs e) { OverlaySettings.AdjustMargin(-OverlaySettings.Step, 0.0); }; actionGrid.Children.Add(leftButton); Button rightButton = CreateDebugButton("右", 40.0); rightButton.Click += delegate(object sender, RoutedEventArgs e) { OverlaySettings.AdjustMargin(OverlaySettings.Step, 0.0); }; actionGrid.Children.Add(rightButton); Button upButton = CreateDebugButton("上", 40.0); upButton.Click += delegate(object sender, RoutedEventArgs e) { OverlaySettings.AdjustMargin(0.0, -OverlaySettings.Step); }; actionGrid.Children.Add(upButton); Button downButton = CreateDebugButton("下", 40.0); downButton.Click += delegate(object sender, RoutedEventArgs e) { OverlaySettings.AdjustMargin(0.0, OverlaySettings.Step); }; actionGrid.Children.Add(downButton); rootPanel.Children.Add(actionGrid); Button applyButton = CreateDebugButton("应用", 0.0); applyButton.Width = 178.0; applyButton.Margin = new Thickness(0.0, 0.0, 0.0, 0.0); applyButton.Click += delegate(object sender, RoutedEventArgs e) { OverlaySettings.ApplyTextValues(); }; rootPanel.Children.Add(applyButton); panelBorder.Child = rootPanel; Panel.SetZIndex(panelBorder, short.MaxValue); return panelBorder; } private static FrameworkElement CreateDebugInputRow(string labelText, string bindingPath) { Grid rowGrid = new Grid(); rowGrid.Margin = new Thickness(0.0, 0.0, 0.0, 6.0); rowGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(44.0) }); rowGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(92.0) }); rowGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(56.0) }); TextBlock label = new TextBlock(); label.Text = labelText; label.Foreground = new SolidColorBrush(Color.FromRgb(210, 221, 236)); label.FontSize = 12.0; label.VerticalAlignment = VerticalAlignment.Center; Grid.SetColumn(label, 0); rowGrid.Children.Add(label); TextBox inputBox = CreateDebugTextBox(bindingPath); Grid.SetColumn(inputBox, 1); rowGrid.Children.Add(inputBox); TextBlock hint = new TextBlock(); hint.Text = "回车"; hint.Foreground = new SolidColorBrush(Color.FromRgb(150, 167, 188)); hint.FontSize = 11.0; hint.VerticalAlignment = VerticalAlignment.Center; hint.HorizontalAlignment = HorizontalAlignment.Right; Grid.SetColumn(hint, 2); rowGrid.Children.Add(hint); return rowGrid; } private static TextBox CreateDebugTextBox(string bindingPath) { TextBox inputBox = new TextBox(); inputBox.Height = 26.0; inputBox.Margin = new Thickness(0.0); inputBox.Padding = new Thickness(6.0, 2.0, 6.0, 2.0); inputBox.Background = new SolidColorBrush(Color.FromRgb(33, 46, 60)); inputBox.BorderBrush = new SolidColorBrush(Color.FromRgb(96, 122, 149)); inputBox.Foreground = Brushes.White; inputBox.FontSize = 12.0; inputBox.VerticalContentAlignment = VerticalAlignment.Center; Binding textBinding = new Binding(bindingPath); textBinding.Source = OverlaySettings; textBinding.Mode = BindingMode.TwoWay; textBinding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged; BindingOperations.SetBinding(inputBox, TextBox.TextProperty, textBinding); inputBox.LostFocus += delegate(object sender, RoutedEventArgs e) { OverlaySettings.ApplyTextValues(); }; inputBox.KeyDown += delegate(object sender, KeyEventArgs e) { if (e.Key != Key.Enter) { return; } OverlaySettings.ApplyTextValues(); e.Handled = true; }; return inputBox; } private static Button CreateDebugButton(string content, double width) { Button button = new Button(); button.Content = content; if (width > 0.0) { button.Width = width; } button.Height = 28.0; button.Margin = new Thickness(0.0, 0.0, 6.0, 0.0); button.Padding = new Thickness(4.0, 0.0, 4.0, 0.0); button.Background = new SolidColorBrush(Color.FromRgb(43, 62, 81)); button.BorderBrush = new SolidColorBrush(Color.FromRgb(96, 122, 149)); button.Foreground = Brushes.White; button.FontSize = 12.0; return button; } private sealed class HeaderDeviceStatusOverlaySettings : PropertyChangedBase, IDisposable { private const double DefaultTopMargin = 8.0; private const double DefaultLeftMargin = 8.0; private const double DefaultDebugPanelTopOffset = 50.0; private const double DefaultAdjustStep = 1.0; private const string ConfigurationDirectoryName = "Configuration"; private const string ConfigurationFileName = "HeaderDeviceStatusOverlay.json"; private readonly string _configurationDirectoryPath; private readonly string _configurationFilePath; private readonly FileSystemWatcher _fileWatcher; private double _topMargin = DefaultTopMargin; private double _leftMargin = DefaultLeftMargin; private bool _isDebugPanelVisible; private double _step = DefaultAdjustStep; private string _leftMarginText = DefaultLeftMargin.ToString("0.##", CultureInfo.InvariantCulture); private string _topMarginText = DefaultTopMargin.ToString("0.##", CultureInfo.InvariantCulture); private string _stepText = DefaultAdjustStep.ToString("0.##", CultureInfo.InvariantCulture); public HeaderDeviceStatusOverlaySettings() { _configurationDirectoryPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ConfigurationDirectoryName); _configurationFilePath = Path.Combine(_configurationDirectoryPath, ConfigurationFileName); EnsureConfigurationFile(); LoadFromFile(); _fileWatcher = new FileSystemWatcher(_configurationDirectoryPath, ConfigurationFileName); _fileWatcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.CreationTime; _fileWatcher.Changed += OnConfigurationFileChanged; _fileWatcher.Created += OnConfigurationFileChanged; _fileWatcher.Renamed += OnConfigurationFileRenamed; _fileWatcher.EnableRaisingEvents = true; } public Thickness Margin { get { return new Thickness(_leftMargin, _topMargin, 0.0, 0.0); } } public Thickness DebugPanelMargin { get { return new Thickness(_leftMargin, _topMargin + DefaultDebugPanelTopOffset, 0.0, 0.0); } } public bool IsDebugPanelVisible { get { return _isDebugPanelVisible; } } public double Step { get { return _step; } } public string LeftMarginText { get { return _leftMarginText; } set { SetAndNotify(ref _leftMarginText, value); } } public string TopMarginText { get { return _topMarginText; } set { SetAndNotify(ref _topMarginText, value); } } public string StepText { get { return _stepText; } set { SetAndNotify(ref _stepText, value); } } public string PositionSummary { get { return string.Format("Left={0:0.##}, Top={1:0.##}, Step={2:0.##}", _leftMargin, _topMargin, _step); } } public void Dispose() { _fileWatcher.Changed -= OnConfigurationFileChanged; _fileWatcher.Created -= OnConfigurationFileChanged; _fileWatcher.Renamed -= OnConfigurationFileRenamed; _fileWatcher.Dispose(); } public void AdjustMargin(double leftOffset, double topOffset) { double leftMargin = _leftMargin + leftOffset; double topMargin = _topMargin + topOffset; UpdateSettings(leftMargin, topMargin, _isDebugPanelVisible, _step); PersistCurrentConfiguration(); } public void HideDebugPanel() { UpdateSettings(_leftMargin, _topMargin, false, _step); PersistCurrentConfiguration(); } public void ApplyTextValues() { double leftMargin; double topMargin; double step; bool canParseLeft = TryParseInput(LeftMarginText, _leftMargin, out leftMargin); bool canParseTop = TryParseInput(TopMarginText, _topMargin, out topMargin); bool canParseStep = TryParseInput(StepText, _step, out step); if (!canParseStep) { step = _step; } step = NormalizeStep(step); UpdateSettings(leftMargin, topMargin, _isDebugPanelVisible, step); PersistCurrentConfiguration(); } private void OnConfigurationFileChanged(object sender, FileSystemEventArgs e) { ReloadOnUiThread(); } private void OnConfigurationFileRenamed(object sender, RenamedEventArgs e) { ReloadOnUiThread(); } private void ReloadOnUiThread() { Application currentApplication = Application.Current; if (currentApplication == null) { LoadFromFile(); return; } currentApplication.Dispatcher.BeginInvoke( DispatcherPriority.Background, new Action(LoadFromFile)); } private void EnsureConfigurationFile() { if (!Directory.Exists(_configurationDirectoryPath)) { Directory.CreateDirectory(_configurationDirectoryPath); } if (File.Exists(_configurationFilePath)) { return; } HeaderDeviceStatusOverlayPositionConfiguration configuration = new HeaderDeviceStatusOverlayPositionConfiguration(); configuration.TopMargin = DefaultTopMargin; configuration.LeftMargin = DefaultLeftMargin; configuration.ShowDebugPanel = false; configuration.AdjustStep = DefaultAdjustStep; SaveConfiguration(configuration); } private void LoadFromFile() { HeaderDeviceStatusOverlayPositionConfiguration configuration = TryReadConfiguration(); if (configuration == null) { return; } double topMargin = NormalizeMargin(configuration.TopMargin, DefaultTopMargin); double leftMargin = NormalizeMargin(configuration.LeftMargin, DefaultLeftMargin); double adjustStep = NormalizeStep(configuration.AdjustStep); UpdateSettings(leftMargin, topMargin, configuration.ShowDebugPanel, adjustStep); } private HeaderDeviceStatusOverlayPositionConfiguration TryReadConfiguration() { for (int attempt = 0; attempt < 3; attempt++) { try { string content = File.ReadAllText(_configurationFilePath); if (string.IsNullOrWhiteSpace(content)) { return new HeaderDeviceStatusOverlayPositionConfiguration(); } HeaderDeviceStatusOverlayPositionConfiguration configuration = JsonConvert.DeserializeObject(content); return configuration ?? new HeaderDeviceStatusOverlayPositionConfiguration(); } catch (IOException) { } catch (UnauthorizedAccessException) { } catch (JsonException) { return null; } } return null; } private void SaveConfiguration(HeaderDeviceStatusOverlayPositionConfiguration configuration) { string content = JsonConvert.SerializeObject(configuration, Formatting.Indented); File.WriteAllText(_configurationFilePath, content); } private void PersistCurrentConfiguration() { HeaderDeviceStatusOverlayPositionConfiguration configuration = new HeaderDeviceStatusOverlayPositionConfiguration(); configuration.TopMargin = _topMargin; configuration.LeftMargin = _leftMargin; configuration.ShowDebugPanel = _isDebugPanelVisible; configuration.AdjustStep = _step; SaveConfiguration(configuration); } private static double NormalizeMargin(double value, double fallbackValue) { if (double.IsNaN(value) || double.IsInfinity(value)) { return fallbackValue; } return value; } private static double NormalizeStep(double value) { if (double.IsNaN(value) || double.IsInfinity(value) || value <= 0.0) { return DefaultAdjustStep; } return value; } private void UpdateSettings(double leftMargin, double topMargin, bool isDebugPanelVisible, double step) { bool hasChanged = !leftMargin.Equals(_leftMargin) || !topMargin.Equals(_topMargin) || !isDebugPanelVisible.Equals(_isDebugPanelVisible) || !step.Equals(_step); if (!hasChanged) { return; } _leftMargin = leftMargin; _topMargin = topMargin; _isDebugPanelVisible = isDebugPanelVisible; _step = step; LeftMarginText = FormatValue(_leftMargin); TopMarginText = FormatValue(_topMargin); StepText = FormatValue(_step); NotifyOfPropertyChange(nameof(Margin)); NotifyOfPropertyChange(nameof(DebugPanelMargin)); NotifyOfPropertyChange(nameof(IsDebugPanelVisible)); NotifyOfPropertyChange(nameof(Step)); NotifyOfPropertyChange(nameof(PositionSummary)); } private static bool TryParseInput(string text, double fallbackValue, out double value) { string trimmedText = string.IsNullOrWhiteSpace(text) ? string.Empty : text.Trim(); if (string.IsNullOrEmpty(trimmedText)) { value = fallbackValue; return false; } return double.TryParse(trimmedText, NumberStyles.Float, CultureInfo.InvariantCulture, out value) || double.TryParse(trimmedText, NumberStyles.Float, CultureInfo.CurrentCulture, out value); } private static string FormatValue(double value) { return value.ToString("0.##", CultureInfo.InvariantCulture); } } private sealed class HeaderDeviceStatusOverlayPositionConfiguration { public double TopMargin { get; set; } public double LeftMargin { get; set; } public bool ShowDebugPanel { get; set; } public double AdjustStep { get; set; } } } }