Separate notification models and views

This fixes language switching while notifications are visible
This commit is contained in:
Ben Olden-Cooligan 2023-04-16 14:35:25 -07:00
parent e871c1f99d
commit fbbdeb3a39
14 changed files with 250 additions and 123 deletions

View File

@ -1,17 +1,9 @@
namespace NAPS2.EtoForms.Notifications;
public class DonateNotification : LinkNotification
public class DonateNotification : NotificationModel
{
private const string DONATE_URL = "https://www.naps2.com/donate";
public DonateNotification() : base(MiscResources.DonatePrompt, MiscResources.Donate, DONATE_URL, null)
public override NotificationView CreateView()
{
HideTimeout = HIDE_LONG;
}
protected override void LinkClick()
{
base.LinkClick();
Manager!.Hide(this);
return new DonateNotificationView(this);
}
}

View File

@ -0,0 +1,18 @@
namespace NAPS2.EtoForms.Notifications;
public class DonateNotificationView : LinkNotificationView
{
private const string DONATE_URL = "https://www.naps2.com/donate";
public DonateNotificationView(DonateNotification model)
: base(model, MiscResources.DonatePrompt, MiscResources.Donate, DONATE_URL, null)
{
HideTimeout = HIDE_LONG;
}
protected override void LinkClick()
{
base.LinkClick();
Manager!.Hide(Model);
}
}

View File

@ -4,16 +4,18 @@ using NAPS2.EtoForms.Layout;
namespace NAPS2.EtoForms.Notifications;
public class LinkNotification : Notification
public class LinkNotificationView : NotificationView
{
private readonly string? _linkTarget;
private readonly string? _folderTarget;
private readonly Label _label = new() { BackgroundColor = BackgroundColor };
private readonly LinkButton _link = new() { BackgroundColor = BackgroundColor };
private readonly LinkButton _link = new() { BackgroundColor = BackgroundColor };
private readonly ContextMenu _contextMenu = new();
protected LinkNotification(string title, string linkLabel, string? linkTarget, string? folderTarget)
protected LinkNotificationView(
NotificationModel model, string title, string linkLabel, string? linkTarget, string? folderTarget)
: base(model)
{
_label.Text = title;
_label.Font = new Font(_label.Font.Family, _label.Font.Size, FontStyle.Bold);

View File

@ -0,0 +1,49 @@
using NAPS2.EtoForms.Layout;
namespace NAPS2.EtoForms.Notifications;
public class NotificationArea : IDisposable
{
private readonly NotificationManager _manager;
private readonly LayoutController _layoutController;
private readonly Dictionary<NotificationModel, (NotificationView, LayoutElement)> _items = new();
public NotificationArea(NotificationManager manager, LayoutController layoutController)
{
_manager = manager;
_layoutController = layoutController;
_manager.Updated += NotificationsUpdated;
UpdateViews();
}
public LayoutColumn Content { get; } = L.Column(C.Filler()).Spacing(20).Padding(15);
private void NotificationsUpdated(object? sender, EventArgs e)
{
UpdateViews();
_layoutController.Invalidate();
}
private void UpdateViews()
{
foreach (var added in _manager.Notifications.Except(_items.Keys).ToList())
{
var view = added.CreateView();
view.Manager = _manager;
var content = view.CreateContent();
Content.Children.Add(content);
_items.Add(added, (view, content));
}
foreach (var removed in _items.Keys.Except(_manager.Notifications).ToList())
{
var (view, content) = _items[removed];
Content.Children.Remove(content);
view.Dispose();
}
}
public void Dispose()
{
_manager.Updated -= NotificationsUpdated;
}
}

View File

@ -1,32 +1,25 @@
using NAPS2.EtoForms.Layout;
namespace NAPS2.EtoForms.Notifications;
public class NotificationManager
{
private readonly Dictionary<Notification, LayoutElement> _items = new();
public LayoutColumn Column { get; } = L.Column(C.Filler()).Spacing(20).Padding(15);
public List<NotificationModel> Notifications { get; } = new();
public event EventHandler? Updated;
public event EventHandler? TimersStarting;
public void Show(Notification notification)
public void Show(NotificationModel notification)
{
notification.Manager = this;
var item = notification.CreateView();
_items[notification] = item;
Column.Children.Add(item);
Notifications.Add(notification);
Updated?.Invoke(this, EventArgs.Empty);
}
public void Hide(Notification notification)
public void Hide(NotificationModel notification)
{
if (!_items.ContainsKey(notification)) return;
Column.Children.Remove(_items[notification]);
Updated?.Invoke(this, EventArgs.Empty);
notification.Dispose();
if (Notifications.Remove(notification))
{
Updated?.Invoke(this, EventArgs.Empty);
}
}
public void StartTimers()

View File

@ -0,0 +1,6 @@
namespace NAPS2.EtoForms.Notifications;
public abstract class NotificationModel
{
public abstract NotificationView CreateView();
}

View File

@ -5,7 +5,7 @@ using NAPS2.EtoForms.Layout;
namespace NAPS2.EtoForms.Notifications;
public abstract class Notification : IDisposable
public abstract class NotificationView : IDisposable
{
// TODO: Get from color scheme
protected static readonly Color BackgroundColor = new(0.95f, 0.95f, 0.95f);
@ -16,6 +16,13 @@ public abstract class Notification : IDisposable
protected const int HIDE_LONG = 60 * 1000;
protected const int HIDE_SHORT = 5 * 1000;
protected NotificationView(NotificationModel model)
{
Model = model;
}
public NotificationModel Model { get; }
protected int HideTimeout { get; set; }
protected bool ShowClose { get; set; } = true;
@ -26,12 +33,12 @@ public abstract class Notification : IDisposable
protected abstract LayoutElement SecondaryContent { get; }
public LayoutElement CreateView()
public LayoutElement CreateContent()
{
var drawable = new Drawable();
drawable.Paint += DrawableOnPaint;
var closeButton = new CloseButton();
closeButton.Click += (_, _) => Manager!.Hide(this);
closeButton.Click += (_, _) => Manager!.Hide(Model);
drawable.MouseUp += (_, _) => NotificationClicked();
drawable.Load += (_, _) => SetUpHideTimeout(drawable);
return L.Overlay(
@ -59,7 +66,7 @@ public abstract class Notification : IDisposable
{
return new Timer(_ =>
{
Manager!.Hide(this);
Manager!.Hide(Model);
}, null, HideTimeout, -1);
}

View File

@ -1,79 +1,18 @@
using Eto.Forms;
using NAPS2.EtoForms.Layout;
namespace NAPS2.EtoForms.Notifications;
public class ProgressNotification : Notification
public class ProgressNotification : NotificationModel
{
private readonly OperationProgress _operationProgress;
private readonly IOperation _op;
private readonly Label _textLabel = new();
private readonly Label _numberLabel = new();
private readonly ProgressBar _progressBar = new();
private readonly LayoutVisibility _numberVis = new(true);
public ProgressNotification(OperationProgress operationProgress, IOperation op)
public ProgressNotification(OperationProgress progress, IOperation op)
{
_operationProgress = operationProgress;
_op = op;
ShowClose = false;
op.StatusChanged += OnStatusChanged;
op.Finished += OnFinished;
if (op.IsFinished)
{
Invoker.Current.Invoke(() => Manager?.Hide(this));
}
_textLabel.MouseUp += (_, _) => NotificationClicked();
_numberLabel.MouseUp += (_, _) => NotificationClicked();
_progressBar.MouseUp += (_, _) => NotificationClicked();
UpdateStatus();
Progress = progress;
Op = op;
}
private void OnStatusChanged(object? sender, EventArgs eventArgs)
public OperationProgress Progress { get; }
public IOperation Op { get; }
public override NotificationView CreateView()
{
Invoker.Current.Invoke(UpdateStatus);
return new ProgressNotificationView(this);
}
private void OnFinished(object? sender, EventArgs e)
{
Invoker.Current.Invoke(() => Manager?.Hide(this));
}
private void UpdateStatus()
{
var text1 = (_textLabel.Text, _numberLabel.Text);
EtoOperationProgress.RenderStatus(_op, _textLabel, _numberLabel, _progressBar);
var text2 = (_textLabel.Text, _numberLabel.Text);
if (text1 != text2)
{
// The text width may have changed, so the notification size could change
Manager?.InvokeUpdated();
}
// Don't display the number if the progress bar is precise
// Otherwise, the widget will be too cluttered
// The number is only shown for OcrOperation at the moment
_numberVis.IsVisible = _op.Status?.IndeterminateProgress == true;
}
protected override void NotificationClicked()
{
Manager!.Hide(this);
_operationProgress.ShowModalProgress(_op);
}
public override void Dispose()
{
base.Dispose();
_op.StatusChanged -= OnStatusChanged;
_op.Finished -= OnFinished;
}
protected override LayoutElement PrimaryContent => L.Row(
_textLabel,
C.Filler().Visible(_numberVis),
_numberLabel.Visible(_numberVis));
protected override LayoutElement SecondaryContent => _progressBar.MaxHeight(10);
}

View File

@ -0,0 +1,80 @@
using Eto.Forms;
using NAPS2.EtoForms.Layout;
namespace NAPS2.EtoForms.Notifications;
public class ProgressNotificationView : NotificationView
{
private readonly OperationProgress _operationProgress;
private readonly IOperation _op;
private readonly Label _textLabel = new();
private readonly Label _numberLabel = new();
private readonly ProgressBar _progressBar = new();
private readonly LayoutVisibility _numberVis = new(true);
public ProgressNotificationView(ProgressNotification model)
: base(model)
{
_operationProgress = model.Progress;
_op = model.Op;
ShowClose = false;
_op.StatusChanged += OnStatusChanged;
_op.Finished += OnFinished;
if (_op.IsFinished)
{
Invoker.Current.Invoke(() => Manager?.Hide(Model));
}
_textLabel.MouseUp += (_, _) => NotificationClicked();
_numberLabel.MouseUp += (_, _) => NotificationClicked();
_progressBar.MouseUp += (_, _) => NotificationClicked();
UpdateStatus();
}
private void OnStatusChanged(object? sender, EventArgs eventArgs)
{
Invoker.Current.Invoke(UpdateStatus);
}
private void OnFinished(object? sender, EventArgs e)
{
Invoker.Current.Invoke(() => Manager?.Hide(Model));
}
private void UpdateStatus()
{
var text1 = (_textLabel.Text, _numberLabel.Text);
EtoOperationProgress.RenderStatus(_op, _textLabel, _numberLabel, _progressBar);
var text2 = (_textLabel.Text, _numberLabel.Text);
if (text1 != text2)
{
// The text width may have changed, so the notification size could change
Manager?.InvokeUpdated();
}
// Don't display the number if the progress bar is precise
// Otherwise, the widget will be too cluttered
// The number is only shown for OcrOperation at the moment
_numberVis.IsVisible = _op.Status?.IndeterminateProgress == true;
}
protected override void NotificationClicked()
{
Manager!.Hide(Model);
_operationProgress.ShowModalProgress(_op);
}
public override void Dispose()
{
base.Dispose();
_op.StatusChanged -= OnStatusChanged;
_op.Finished -= OnFinished;
}
protected override LayoutElement PrimaryContent => L.Row(
_textLabel,
C.Filler().Visible(_numberVis),
_numberLabel.Visible(_numberVis));
protected override LayoutElement SecondaryContent => _progressBar.MaxHeight(10);
}

View File

@ -1,10 +1,18 @@
namespace NAPS2.EtoForms.Notifications;
public class SaveNotification : LinkNotification
public class SaveNotification : NotificationModel
{
public SaveNotification(string message, string path)
: base(message, Path.GetFileName(path), path, Path.GetDirectoryName(path))
public SaveNotification(string title, string path)
{
HideTimeout = HIDE_SHORT;
Title = title;
Path = path;
}
public string Title { get; }
public string Path { get; }
public override NotificationView CreateView()
{
return new SaveNotificationView(this);
}
}

View File

@ -0,0 +1,10 @@
namespace NAPS2.EtoForms.Notifications;
public class SaveNotificationView : LinkNotificationView
{
public SaveNotificationView(SaveNotification model)
: base(model, model.Title, Path.GetFileName(model.Path), model.Path, Path.GetDirectoryName(model.Path))
{
HideTimeout = HIDE_SHORT;
}
}

View File

@ -2,25 +2,19 @@ using NAPS2.Update;
namespace NAPS2.EtoForms.Notifications;
public class UpdateNotification : LinkNotification
public class UpdateNotification : NotificationModel
{
private readonly IUpdateChecker _updateChecker;
private readonly UpdateInfo _update;
public UpdateNotification(IUpdateChecker updateChecker, UpdateInfo update)
: base(
MiscResources.UpdateAvailable,
string.Format(MiscResources.Install, update.Name),
null, null)
{
_updateChecker = updateChecker;
_update = update;
HideTimeout = HIDE_LONG;
UpdateChecker = updateChecker;
Update = update;
}
protected override void LinkClick()
public IUpdateChecker UpdateChecker { get; }
public UpdateInfo Update { get; }
public override NotificationView CreateView()
{
_updateChecker.StartUpdate(_update);
Manager!.Hide(this);
return new UpdateNotificationView(this);
}
}

View File

@ -0,0 +1,27 @@
using NAPS2.Update;
namespace NAPS2.EtoForms.Notifications;
public class UpdateNotificationView : LinkNotificationView
{
private readonly IUpdateChecker _updateChecker;
private readonly UpdateInfo _update;
public UpdateNotificationView(UpdateNotification model)
: base(
model,
MiscResources.UpdateAvailable,
string.Format(MiscResources.Install, model.Update.Name),
null, null)
{
_updateChecker = model.UpdateChecker;
_update = model.Update;
HideTimeout = HIDE_LONG;
}
protected override void LinkClick()
{
_updateChecker.StartUpdate(_update);
Manager!.Hide(Model);
}
}

View File

@ -32,6 +32,7 @@ public abstract class DesktopForm : EtoFormBase
private readonly ListProvider<Command> _languageMenuCommands = new();
private readonly ContextMenu _contextMenu = new();
private readonly NotificationArea _notificationArea;
protected IListView<UiImage> _listView;
private ImageListSyncer? _imageListSyncer;
@ -109,7 +110,7 @@ public abstract class DesktopForm : EtoFormBase
ImageList.SelectionChanged += ImageList_SelectionChanged;
ImageList.ImagesUpdated += ImageList_ImagesUpdated;
_profileManager.ProfilesUpdated += ProfileManager_ProfilesUpdated;
_notificationManager.Updated += (_, _) => LayoutController.Invalidate();
_notificationArea = new NotificationArea(_notificationManager, LayoutController);
}
protected override void BuildLayout()
@ -127,7 +128,7 @@ public abstract class DesktopForm : EtoFormBase
L.Row(
GetZoomButtons(),
C.Filler(),
_notificationManager.Column)
_notificationArea.Content)
).Padding(10)
);
@ -232,6 +233,7 @@ public abstract class DesktopForm : EtoFormBase
ImageList.SelectionChanged -= ImageList_SelectionChanged;
ImageList.ImagesUpdated -= ImageList_ImagesUpdated;
_profileManager.ProfilesUpdated -= ProfileManager_ProfilesUpdated;
_notificationArea.Dispose();
_imageListSyncer?.Dispose();
}