2
0
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:
Xjph
2021-11-15 10:57:46 -03:30
committed by GitHub
parent 9ad3f77bb8
commit 554948534e
22 changed files with 1144 additions and 61 deletions

View 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();
}
}
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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;

View File

@ -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; }

View File

@ -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;
}
}

View File

@ -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);