mirror of
https://github.com/9ParsonsB/Pulsar.git
synced 2025-07-01 08:23:42 -04:00
observatory herald (#30)
* WIP: initial commit for observatory herald * Plugin error handling refactor * make error window non-modal * tidy up plugin error handling * first pass for basic herald functionality * corrections for linux env * Use FNV hash directly instead of managing through dictionary/index file * resolve audio queuing issue, switch to personal NetCoreAudio fork * merge cleanup * add enable setting, populate defaults * framework xml doc update * Adjust settings, add style selection, replace locale with demonym in dropdown list. * Test is position is on screen before saving/loading. * use a default that's actually in the list
This commit is contained in:
26
ObservatoryCore/ErrorReporter.cs
Normal file
26
ObservatoryCore/ErrorReporter.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Observatory
|
||||
{
|
||||
public static class ErrorReporter
|
||||
{
|
||||
public static void ShowErrorPopup(string title, string message)
|
||||
{
|
||||
if (Avalonia.Application.Current.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
var errorMessage = MessageBox.Avalonia.MessageBoxManager
|
||||
.GetMessageBoxStandardWindow(new MessageBox.Avalonia.DTO.MessageBoxStandardParams
|
||||
{
|
||||
ContentTitle = title,
|
||||
ContentMessage = message,
|
||||
Topmost = true
|
||||
});
|
||||
errorMessage.Show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -318,18 +318,7 @@ namespace Observatory
|
||||
}
|
||||
}
|
||||
|
||||
if (Avalonia.Application.Current.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
var errorMessage = MessageBox.Avalonia.MessageBoxManager
|
||||
.GetMessageBoxStandardWindow(new MessageBox.Avalonia.DTO.MessageBoxStandardParams
|
||||
{
|
||||
ContentTitle = $"Journal Read Error{(readErrors.Count > 1 ? "s" : "")}",
|
||||
ContentMessage = errorContent.ToString()
|
||||
});
|
||||
errorMessage.ShowDialog(desktop.MainWindow);
|
||||
|
||||
}
|
||||
|
||||
ErrorReporter.ShowErrorPopup($"Journal Read Error{(readErrors.Count > 1 ? "s" : "")}", errorContent.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,36 +85,12 @@ namespace Observatory.NativeNotification
|
||||
XmlNamespaceManager ssmlNs = new(ssmlDoc.NameTable);
|
||||
ssmlNs.AddNamespace("ssml", ssmlNamespace);
|
||||
|
||||
//If the SSML already has a voice element leave it alone.
|
||||
if (ssmlDoc.SelectSingleNode("/ssml:speak/ssml:voice", ssmlNs) == null)
|
||||
{
|
||||
//Preserve existing content to place it in new voice element
|
||||
string speakContent = ssmlDoc.DocumentElement.InnerXml;
|
||||
speakContent = speakContent.Replace($"xmlns=\"{ssmlNs.LookupNamespace("ssml")}\"", string.Empty);
|
||||
|
||||
//Crete new voice element and name attribute objects
|
||||
var voiceElement = ssmlDoc.CreateElement("voice", ssmlNs.LookupNamespace("ssml"));
|
||||
var voiceAttribute = ssmlDoc.CreateAttribute("name");
|
||||
var voiceNode = ssmlDoc.SelectSingleNode("/ssml:speak/ssml:voice", ssmlNs);
|
||||
|
||||
//Update content of new element
|
||||
voiceAttribute.Value = voiceName;
|
||||
voiceElement.Attributes.Append(voiceAttribute);
|
||||
voiceElement.InnerXml = speakContent;
|
||||
|
||||
//Clear existing content and insert new element
|
||||
ssmlDoc.DocumentElement.InnerText = string.Empty;
|
||||
ssmlDoc.DocumentElement.AppendChild(voiceElement);
|
||||
voiceNode.Attributes.GetNamedItem("name").Value = voiceName;
|
||||
|
||||
ssml = ssmlDoc.OuterXml;
|
||||
}
|
||||
|
||||
//If I leave the namespace in speakContent above it's left behind as a redundant
|
||||
//attribute which breaks the speech generation.
|
||||
//If I remove it then the XmlDoc explicitly adds an empty namespace which *also*
|
||||
//breaks speech generation.
|
||||
//The empty one is easier to remove later, so that's what I'm doing, but if someone
|
||||
//has a better suggestion I'm all for it.
|
||||
return ssml.Replace("xmlns=\"\"", string.Empty);
|
||||
return ssmlDoc.OuterXml;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,18 +14,32 @@ namespace Observatory.PluginManagement
|
||||
{
|
||||
private IEnumerable<IObservatoryWorker> observatoryWorkers;
|
||||
private IEnumerable<IObservatoryNotifier> observatoryNotifiers;
|
||||
private List<string> errorList;
|
||||
|
||||
public PluginEventHandler(IEnumerable<IObservatoryWorker> observatoryWorkers, IEnumerable<IObservatoryNotifier> observatoryNotifiers)
|
||||
{
|
||||
this.observatoryWorkers = observatoryWorkers;
|
||||
this.observatoryNotifiers = observatoryNotifiers;
|
||||
errorList = new();
|
||||
}
|
||||
|
||||
public void OnJournalEvent(object source, JournalEventArgs journalEventArgs)
|
||||
{
|
||||
foreach (var worker in observatoryWorkers)
|
||||
{
|
||||
worker.JournalEvent((JournalBase)journalEventArgs.journalEvent);
|
||||
try
|
||||
{
|
||||
worker.JournalEvent((JournalBase)journalEventArgs.journalEvent);
|
||||
}
|
||||
catch (PluginException ex)
|
||||
{
|
||||
RecordError(ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
RecordError(ex, worker.Name, journalEventArgs.journalType.Name);
|
||||
}
|
||||
ReportErrorsIfAny();
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,7 +47,19 @@ namespace Observatory.PluginManagement
|
||||
{
|
||||
foreach (var worker in observatoryWorkers)
|
||||
{
|
||||
worker.StatusChange((Status)journalEventArgs.journalEvent);
|
||||
try
|
||||
{
|
||||
worker.StatusChange((Status)journalEventArgs.journalEvent);
|
||||
}
|
||||
catch (PluginException ex)
|
||||
{
|
||||
RecordError(ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
RecordError(ex, worker.Name, journalEventArgs.journalType.Name);
|
||||
}
|
||||
ReportErrorsIfAny();
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,7 +67,39 @@ namespace Observatory.PluginManagement
|
||||
{
|
||||
foreach (var notifier in observatoryNotifiers)
|
||||
{
|
||||
notifier.OnNotificationEvent(notificationArgs);
|
||||
try
|
||||
{
|
||||
notifier.OnNotificationEvent(notificationArgs);
|
||||
}
|
||||
catch (PluginException ex)
|
||||
{
|
||||
RecordError(ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
RecordError(ex, notifier.Name, notificationArgs.Title);
|
||||
}
|
||||
ReportErrorsIfAny();
|
||||
}
|
||||
}
|
||||
|
||||
private void RecordError(PluginException ex)
|
||||
{
|
||||
errorList.Add($"Error in {ex.PluginName}: {ex.Message}");
|
||||
}
|
||||
|
||||
private void RecordError(Exception ex, string plugin, string eventType)
|
||||
{
|
||||
errorList.Add($"Error in {plugin} while handling {eventType}: {ex.Message}");
|
||||
}
|
||||
|
||||
private void ReportErrorsIfAny()
|
||||
{
|
||||
if (errorList.Any())
|
||||
{
|
||||
ErrorReporter.ShowErrorPopup($"Plugin Error{(errorList.Count > 1 ? "s" : "")}", string.Join(Environment.NewLine, errorList));
|
||||
|
||||
errorList.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ using System.Reflection;
|
||||
using System.Data;
|
||||
using Observatory.Framework.Interfaces;
|
||||
using System.IO;
|
||||
using System.Configuration;
|
||||
using Observatory.Framework;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Observatory.PluginManagement
|
||||
@ -39,11 +39,6 @@ namespace Observatory.PluginManagement
|
||||
{
|
||||
errorList = LoadPlugins(out workerPlugins, out notifyPlugins);
|
||||
|
||||
foreach (var error in errorList)
|
||||
{
|
||||
Console.WriteLine(error);
|
||||
}
|
||||
|
||||
var pluginHandler = new PluginEventHandler(workerPlugins.Select(p => p.plugin), notifyPlugins.Select(p => p.plugin));
|
||||
var logMonitor = LogMonitor.GetInstance;
|
||||
pluginPanels = new();
|
||||
@ -54,24 +49,53 @@ namespace Observatory.PluginManagement
|
||||
|
||||
var core = new PluginCore();
|
||||
|
||||
List<IObservatoryPlugin> errorPlugins = new();
|
||||
|
||||
foreach (var plugin in workerPlugins.Select(p => p.plugin))
|
||||
{
|
||||
LoadSettings(plugin);
|
||||
plugin.Load(core);
|
||||
try
|
||||
{
|
||||
LoadSettings(plugin);
|
||||
plugin.Load(core);
|
||||
}
|
||||
catch (PluginException ex)
|
||||
{
|
||||
errorList.Add(FormatErrorMessage(ex));
|
||||
errorPlugins.Add(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
workerPlugins.RemoveAll(w => errorPlugins.Contains(w.plugin));
|
||||
errorPlugins.Clear();
|
||||
|
||||
foreach (var plugin in notifyPlugins.Select(p => p.plugin))
|
||||
{
|
||||
// Notifiers which are also workers need not be loaded again (they are the same instance).
|
||||
if (!plugin.GetType().IsAssignableTo(typeof(IObservatoryWorker)))
|
||||
{
|
||||
LoadSettings(plugin);
|
||||
plugin.Load(core);
|
||||
try
|
||||
{
|
||||
LoadSettings(plugin);
|
||||
plugin.Load(core);
|
||||
}
|
||||
catch (PluginException ex)
|
||||
{
|
||||
errorList.Add(FormatErrorMessage(ex));
|
||||
errorPlugins.Add(plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyPlugins.RemoveAll(n => errorPlugins.Contains(n.plugin));
|
||||
|
||||
core.Notification += pluginHandler.OnNotificationEvent;
|
||||
}
|
||||
|
||||
private static string FormatErrorMessage(PluginException ex)
|
||||
{
|
||||
return $"{ex.PluginName}: {ex.UserMessage}";
|
||||
}
|
||||
|
||||
private void LoadSettings(IObservatoryPlugin plugin)
|
||||
{
|
||||
string savedSettings = Properties.Core.Default.PluginSettings;
|
||||
|
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
|
||||
namespace Observatory.UI.ViewModels
|
||||
{
|
||||
@ -9,6 +9,9 @@ namespace Observatory.UI.ViewModels
|
||||
public MainWindowViewModel(PluginManagement.PluginManager pluginManager)
|
||||
{
|
||||
core = new CoreViewModel(pluginManager.workerPlugins, pluginManager.notifyPlugins);
|
||||
|
||||
if (pluginManager.errorList.Any())
|
||||
ErrorReporter.ShowErrorPopup("Plugin Load Error", string.Join(Environment.NewLine, pluginManager.errorList));
|
||||
}
|
||||
|
||||
public CoreViewModel core { get; }
|
||||
|
@ -535,7 +535,7 @@ namespace Observatory.UI.Views
|
||||
NotificationArgs args = new()
|
||||
{
|
||||
Title = "Speech Synthesis Test",
|
||||
TitleSsml = "<speak version=\"1.0\" xmlns=\"http://www.w3.org/2001/10/synthesis\" xml:lang=\"en-US\">Speech Synthesis Test</speak>",
|
||||
TitleSsml = "<speak version=\"1.0\" xmlns=\"http://www.w3.org/2001/10/synthesis\" xml:lang=\"en-US\"><voice name=\"\">Speech Synthesis Test</voice></speak>",
|
||||
Detail = harvardSentences.OrderBy(s => new Random().NextDouble()).First()
|
||||
};
|
||||
|
||||
@ -943,6 +943,46 @@ namespace Observatory.UI.Views
|
||||
|
||||
settingsGrid.AddControl(actionButton, settingsGrid.RowDefinitions.Count - 1, 0);
|
||||
|
||||
break;
|
||||
case Dictionary<string, object> dictSetting:
|
||||
|
||||
var backingValueName = (SettingBackingValue)Attribute.GetCustomAttribute(setting.Key, typeof(SettingBackingValue));
|
||||
|
||||
var backingValue = from s in displayedSettings
|
||||
where s.Value == backingValueName.BackingProperty
|
||||
select s.Key;
|
||||
|
||||
if (backingValue.Count() != 1)
|
||||
throw new($"{plugin.ShortName}: Dictionary settings must have exactly one backing value.");
|
||||
|
||||
label.Text += ": ";
|
||||
|
||||
ComboBox selectionDropDown = new()
|
||||
{
|
||||
MinWidth = 200
|
||||
};
|
||||
|
||||
selectionDropDown.Items = from s in dictSetting
|
||||
orderby s.Key
|
||||
select s.Key;
|
||||
|
||||
string currentSelection = backingValue.First().GetValue(plugin.Settings)?.ToString();
|
||||
|
||||
if (currentSelection?.Length > 0)
|
||||
{
|
||||
selectionDropDown.SelectedItem = currentSelection;
|
||||
}
|
||||
|
||||
selectionDropDown.SelectionChanged += (object sender, SelectionChangedEventArgs e) =>
|
||||
{
|
||||
var comboBox = (ComboBox)sender;
|
||||
backingValue.First().SetValue(plugin.Settings, comboBox.SelectedItem.ToString());
|
||||
PluginManagement.PluginManager.GetInstance.SaveSettings(plugin, plugin.Settings);
|
||||
};
|
||||
|
||||
settingsGrid.AddControl(label, settingsGrid.RowDefinitions.Count - 1, 0);
|
||||
settingsGrid.AddControl(selectionDropDown, settingsGrid.RowDefinitions.Count - 1, 1);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -15,17 +15,43 @@ namespace Observatory.UI.Views
|
||||
#endif
|
||||
Height = Properties.Core.Default.MainWindowSize.Height;
|
||||
Width = Properties.Core.Default.MainWindowSize.Width;
|
||||
Position = new PixelPoint(Properties.Core.Default.MainWindowPosition.X, Properties.Core.Default.MainWindowPosition.Y);
|
||||
|
||||
var savedPosition = new System.Drawing.Point(Properties.Core.Default.MainWindowPosition.X, Properties.Core.Default.MainWindowPosition.Y);
|
||||
if (PointWithinDesktopWorkingArea(savedPosition))
|
||||
Position = new PixelPoint(Properties.Core.Default.MainWindowPosition.X, Properties.Core.Default.MainWindowPosition.Y);
|
||||
|
||||
Closing += (object sender, System.ComponentModel.CancelEventArgs e) =>
|
||||
{
|
||||
var size = new System.Drawing.Size((int)System.Math.Round(Width), (int)System.Math.Round(Height));
|
||||
var position = new System.Drawing.Point(Position.X, Position.Y);
|
||||
Properties.Core.Default.MainWindowSize = size;
|
||||
Properties.Core.Default.MainWindowPosition = position;
|
||||
|
||||
var position = new System.Drawing.Point(Position.X, Position.Y);
|
||||
if (PointWithinDesktopWorkingArea(position))
|
||||
Properties.Core.Default.MainWindowPosition = position;
|
||||
|
||||
Properties.Core.Default.Save();
|
||||
};
|
||||
}
|
||||
|
||||
private bool PointWithinDesktopWorkingArea(System.Drawing.Point position)
|
||||
{
|
||||
bool inBounds = false;
|
||||
|
||||
foreach (var screen in Screens.All)
|
||||
{
|
||||
if (screen.WorkingArea.TopLeft.X <= position.X
|
||||
&& screen.WorkingArea.TopLeft.Y <= position.Y
|
||||
&& screen.WorkingArea.BottomRight.X > position.X
|
||||
&& screen.WorkingArea.BottomRight.Y > position.Y)
|
||||
{
|
||||
inBounds = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return inBounds;
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
|
Reference in New Issue
Block a user