SEBWIN-312: Started implementing task view.

This commit is contained in:
dbuechel 2019-11-15 16:00:03 +01:00
parent 5f31656649
commit 08bf49b61b
19 changed files with 248 additions and 33 deletions

View file

@ -28,6 +28,6 @@ namespace SafeExamBrowser.Applications.Contracts
/// <summary>
/// The resource providing the application icon.
/// </summary>
public IconResource IconResource { get; set; }
public IconResource Icon { get; set; }
}
}

View file

@ -7,6 +7,7 @@
*/
using SafeExamBrowser.Applications.Contracts.Events;
using SafeExamBrowser.Core.Contracts;
namespace SafeExamBrowser.Applications.Contracts
{
@ -15,6 +16,11 @@ namespace SafeExamBrowser.Applications.Contracts
/// </summary>
public interface IApplicationInstance
{
/// <summary>
/// The icon resource for this instance.
/// </summary>
IconResource Icon { get; }
/// <summary>
/// The unique identifier for the application instance.
/// </summary>

View file

@ -64,7 +64,7 @@ namespace SafeExamBrowser.Applications
private IApplication BuildApplication(string executablePath, WhitelistApplication settings)
{
var icon = new IconResource { Type = IconResourceType.Embedded, Uri = new Uri(executablePath) };
var info = new ApplicationInfo { IconResource = icon, Name = settings.DisplayName, Tooltip = settings.Description ?? settings.DisplayName };
var info = new ApplicationInfo { Icon = icon, Name = settings.DisplayName, Tooltip = settings.Description ?? settings.DisplayName };
var application = new ExternalApplication(executablePath, info, logger.CloneFor(settings.DisplayName), processFactory);
return application;

View file

@ -44,9 +44,11 @@ namespace SafeExamBrowser.Applications
{
logger.Info("Starting application...");
// TODO: Ensure that SEB does not crash if an application cannot be started!!
var process = processFactory.StartNew(executablePath);
var id = new ApplicationInstanceIdentifier(process.Id);
var instance = new ExternalApplicationInstance(id, logger.CloneFor($"{Info.Name} {id}"), process);
var instance = new ExternalApplicationInstance(Info.Icon, id, logger.CloneFor($"{Info.Name} {id}"), process);
instance.Initialize();
instances.Add(instance);

View file

@ -10,6 +10,7 @@ using System;
using System.Timers;
using SafeExamBrowser.Applications.Contracts;
using SafeExamBrowser.Applications.Contracts.Events;
using SafeExamBrowser.Core.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.WindowsApi.Contracts;
@ -24,6 +25,7 @@ namespace SafeExamBrowser.Applications
private IProcess process;
private Timer timer;
public IconResource Icon { get; }
public InstanceIdentifier Id { get; }
public string Name { get; }
@ -31,8 +33,9 @@ namespace SafeExamBrowser.Applications
public event NameChangedEventHandler NameChanged;
public event InstanceTerminatedEventHandler Terminated;
public ExternalApplicationInstance(InstanceIdentifier id, ILogger logger, IProcess process)
public ExternalApplicationInstance(IconResource icon, InstanceIdentifier id, ILogger logger, IProcess process)
{
this.Icon = icon;
this.Id = id;
this.logger = logger;
this.process = process;

View file

@ -65,7 +65,7 @@ namespace SafeExamBrowser.Browser
var cefSettings = InitializeCefSettings();
var success = Cef.Initialize(cefSettings, true, default(IApp));
Info = BuildApplicationInfo();
InitializeApplicationInfo();
if (success)
{
@ -95,11 +95,11 @@ namespace SafeExamBrowser.Browser
logger.Info("Terminated browser.");
}
private ApplicationInfo BuildApplicationInfo()
private void InitializeApplicationInfo()
{
return new ApplicationInfo
Info = new ApplicationInfo
{
IconResource = new BrowserIconResource(),
Icon = new BrowserIconResource(),
Name = "Safe Exam Browser",
Tooltip = text.Get(TextKey.Browser_Tooltip)
};

View file

@ -17,6 +17,7 @@ using SafeExamBrowser.Browser.Events;
using SafeExamBrowser.Browser.Filters;
using SafeExamBrowser.Browser.Handlers;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Core.Contracts;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Settings.Browser;
@ -48,6 +49,7 @@ namespace SafeExamBrowser.Browser
get { return isMainInstance ? settings.MainWindow : settings.AdditionalWindow; }
}
public IconResource Icon { get; private set; }
public InstanceIdentifier Id { get; private set; }
public string Name { get; private set; }
@ -108,6 +110,8 @@ namespace SafeExamBrowser.Browser
var requestLogger = logger.CloneFor($"{nameof(RequestHandler)} {Id}");
var requestHandler = new RequestHandler(appConfig, settings.Filter, requestFilter, requestLogger, text);
Icon = new BrowserIconResource();
displayHandler.FaviconChanged += DisplayHandler_FaviconChanged;
displayHandler.ProgressChanged += DisplayHandler_ProgressChanged;
downloadHandler.ConfigurationDownloadRequested += DownloadHandler_ConfigurationDownloadRequested;
@ -201,10 +205,10 @@ namespace SafeExamBrowser.Browser
{
if (task.IsCompleted && task.Result.IsSuccessStatusCode)
{
var icon = new BrowserIconResource(uri);
Icon = new BrowserIconResource(uri);
IconChanged?.Invoke(icon);
window.UpdateIcon(icon);
IconChanged?.Invoke(Icon);
window.UpdateIcon(Icon);
}
});
}

View file

@ -98,14 +98,12 @@ namespace SafeExamBrowser.Client.UnitTests.Operations
var terminationActivator = new Mock<ITerminationActivator>();
context.Activators.Add(actionCenterActivator.Object);
context.Activators.Add(taskViewActivator.Object);
context.Activators.Add(terminationActivator.Object);
context.Settings.ActionCenter.EnableActionCenter = true;
sut.Perform();
actionCenterActivator.Verify(a => a.Start(), Times.Once);
taskViewActivator.Verify(a => a.Start(), Times.Once);
terminationActivator.Verify(a => a.Start(), Times.Once);
}
@ -176,6 +174,7 @@ namespace SafeExamBrowser.Client.UnitTests.Operations
[TestMethod]
public void Perform_MustInitializeTaskView()
{
// Only start activator if ALT+TAB enabled!
Assert.Fail("TODO");
}

View file

@ -113,7 +113,7 @@ namespace SafeExamBrowser.Client.Operations
actionCenterActivator.Start();
}
if (activator is ITaskViewActivator taskViewActivator)
if (Context.Settings.Keyboard.AllowAltTab && activator is ITaskViewActivator taskViewActivator)
{
taskView.Register(taskViewActivator);
taskViewActivator.Start();

View file

@ -16,13 +16,18 @@ namespace SafeExamBrowser.UserInterface.Contracts.Shell
public interface ITaskViewActivator : IActivator
{
/// <summary>
/// Fired when the next application instance should be selected.
/// Fired when the task view should be hidden.
/// </summary>
event ActivatorEventHandler Next;
event ActivatorEventHandler Deactivated;
/// <summary>
/// Fired when the previous application instance should be selected.
/// Fired when the task view should be made visible and the next application instance should be selected.
/// </summary>
event ActivatorEventHandler Previous;
event ActivatorEventHandler NextActivated;
/// <summary>
/// Fired when the task view should be made visible and the previous application instance should be selected.
/// </summary>
event ActivatorEventHandler PreviousActivated;
}
}

View file

@ -32,7 +32,7 @@ namespace SafeExamBrowser.UserInterface.Desktop.Controls
private void InitializeApplicationInstanceButton()
{
Icon.Content = IconResourceLoader.Load(info.IconResource);
Icon.Content = IconResourceLoader.Load(info.Icon);
Text.Text = instance?.Name ?? info.Name;
Button.Click += (o, args) => Clicked?.Invoke(this, EventArgs.Empty);
Button.ToolTip = instance?.Name ?? info.Tooltip;

View file

@ -37,7 +37,7 @@ namespace SafeExamBrowser.UserInterface.Desktop.Controls
application.InstanceStarted += Application_InstanceStarted;
Button.Click += Button_Click;
Button.Content = IconResourceLoader.Load(application.Info.IconResource);
Button.Content = IconResourceLoader.Load(application.Info.Icon);
Button.MouseEnter += (o, args) => InstancePopup.IsOpen = InstanceStackPanel.Children.Count > 1;
Button.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => InstancePopup.IsOpen = InstancePopup.IsMouseOver));
Button.ToolTip = application.Info.Tooltip;

View file

@ -32,7 +32,7 @@ namespace SafeExamBrowser.UserInterface.Desktop.Controls
{
Button.Click += Button_Click;
Button.ToolTip = instance.Name;
Icon.Content = IconResourceLoader.Load(info.IconResource);
Icon.Content = IconResourceLoader.Load(info.Icon);
instance.IconChanged += Instance_IconChanged;
instance.NameChanged += Instance_NameChanged;
Text.Text = instance.Name;

View file

@ -4,9 +4,9 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Desktop"
mc:Ignorable="d" AllowsTransparency="True" Background="#74000000" BorderBrush="DodgerBlue" BorderThickness="1" Title="TaskView"
Height="450" Width="800" WindowStartupLocation="CenterScreen" WindowStyle="None">
mc:Ignorable="d" AllowsTransparency="True" Background="#AA000000" BorderBrush="DodgerBlue" BorderThickness="1" Title="TaskView"
Topmost="True" Height="450" SizeToContent="WidthAndHeight" Width="800" WindowStartupLocation="CenterScreen" WindowStyle="None">
<Grid>
<StackPanel Name="Rows" Margin="10" Orientation="Vertical" />
</Grid>
</Window>

View file

@ -6,27 +6,156 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using SafeExamBrowser.Applications.Contracts;
using SafeExamBrowser.Core.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Shell;
using SafeExamBrowser.UserInterface.Shared.Utilities;
namespace SafeExamBrowser.UserInterface.Desktop
{
public partial class TaskView : Window, ITaskView
{
private IList<IApplicationInstance> instances;
public TaskView()
{
InitializeComponent();
instances = new List<IApplicationInstance>();
}
public void Add(IApplication application)
{
application.InstanceStarted += Application_InstanceStarted;
}
public void Register(ITaskViewActivator activator)
{
activator.Deactivated += Activator_Deactivated;
activator.NextActivated += Activator_Next;
activator.PreviousActivated += Activator_Previous;
}
private void Application_InstanceStarted(IApplicationInstance instance)
{
Dispatcher.InvokeAsync(() =>
{
instance.IconChanged += Instance_IconChanged;
instance.NameChanged += Instance_NameChanged;
instance.Terminated += Instance_Terminated;
instances.Add(instance);
Update();
});
}
private void Activator_Deactivated()
{
Dispatcher.InvokeAsync(Hide);
}
private void Activator_Next()
{
Dispatcher.InvokeAsync(ShowConditional);
}
private void Activator_Previous()
{
Dispatcher.InvokeAsync(ShowConditional);
}
private void Instance_IconChanged(IconResource icon)
{
// TODO Dispatcher.InvokeAsync(...);
}
private void Instance_NameChanged(string name)
{
// TODO Dispatcher.InvokeAsync(...);
}
private void Instance_Terminated(InstanceIdentifier id)
{
Dispatcher.InvokeAsync(() =>
{
var instance = instances.FirstOrDefault(i => i.Id == id);
if (instance != default(IApplicationInstance))
{
instances.Remove(instance);
Update();
}
});
}
private void ShowConditional()
{
if (Visibility != Visibility.Visible && instances.Any())
{
Show();
}
}
private void Update()
{
var max = Math.Ceiling(Math.Sqrt(instances.Count));
var stack = new Stack<IApplicationInstance>(instances);
Rows.Children.Clear();
for (var rowCount = 0; rowCount < max && stack.Any(); rowCount++)
{
var row = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Center };
Rows.Children.Add(row);
for (var columnIndex = 0; columnIndex < max && stack.Any(); columnIndex++)
{
var instance = stack.Pop();
var control = BuildInstanceControl(instance);
row.Children.Add(control);
}
}
UpdateLayout();
Left = (SystemParameters.WorkArea.Width - Width) / 2 + SystemParameters.WorkArea.Left;
Top = (SystemParameters.WorkArea.Height - Height) / 2 + SystemParameters.WorkArea.Top;
if (!instances.Any())
{
Hide();
}
}
private UIElement BuildInstanceControl(IApplicationInstance instance)
{
var border = new Border();
var stackPanel = new StackPanel();
var icon = IconResourceLoader.Load(instance.Icon);
border.BorderBrush = Brushes.White;
border.BorderThickness = new Thickness(1);
border.Height = 150;
border.Margin = new Thickness(5);
border.Padding = new Thickness(2);
border.Width = 250;
border.Child = stackPanel;
stackPanel.HorizontalAlignment = HorizontalAlignment.Center;
stackPanel.Orientation = Orientation.Vertical;
stackPanel.VerticalAlignment = VerticalAlignment.Center;
stackPanel.Children.Add(new ContentControl { Content = icon, MaxWidth = 50 });
stackPanel.Children.Add(new TextBlock(new Run($"Instance {instance.Name ?? "NULL"}") { Foreground = Brushes.White, FontWeight = FontWeights.Bold }));
return border;
}
}
}

View file

@ -32,7 +32,7 @@ namespace SafeExamBrowser.UserInterface.Mobile.Controls
private void InitializeApplicationInstanceButton()
{
Icon.Content = IconResourceLoader.Load(info.IconResource);
Icon.Content = IconResourceLoader.Load(info.Icon);
Text.Text = instance?.Name ?? info.Name;
Button.Click += (o, args) => Clicked?.Invoke(this, EventArgs.Empty);
Button.ToolTip = instance?.Name ?? info.Tooltip;

View file

@ -37,7 +37,7 @@ namespace SafeExamBrowser.UserInterface.Mobile.Controls
application.InstanceStarted += Application_InstanceStarted;
Button.Click += Button_Click;
Button.Content = IconResourceLoader.Load(application.Info.IconResource);
Button.Content = IconResourceLoader.Load(application.Info.Icon);
Button.MouseEnter += (o, args) => InstancePopup.IsOpen = InstanceStackPanel.Children.Count > 1;
Button.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => InstancePopup.IsOpen = InstancePopup.IsMouseOver));
Button.ToolTip = application.Info.Tooltip;

View file

@ -32,7 +32,7 @@ namespace SafeExamBrowser.UserInterface.Mobile.Controls
{
Button.Click += Button_Click;
Button.ToolTip = instance.Name;
Icon.Content = IconResourceLoader.Load(info.IconResource);
Icon.Content = IconResourceLoader.Load(info.Icon);
instance.IconChanged += Instance_IconChanged;
instance.NameChanged += Instance_NameChanged;
Text.Text = instance.Name;

View file

@ -17,10 +17,12 @@ namespace SafeExamBrowser.UserInterface.Shared.Activators
{
public class TaskViewKeyboardActivator : KeyboardActivator, ITaskViewActivator
{
private bool Activated, BlockActivation, LeftShift, Tab;
private ILogger logger;
public event ActivatorEventHandler Next;
public event ActivatorEventHandler Previous;
public event ActivatorEventHandler Deactivated;
public event ActivatorEventHandler NextActivated;
public event ActivatorEventHandler PreviousActivated;
public TaskViewKeyboardActivator(ILogger logger, INativeMethods nativeMethods) : base(nativeMethods)
{
@ -29,17 +31,82 @@ namespace SafeExamBrowser.UserInterface.Shared.Activators
public void Pause()
{
Paused = true;
BlockActivation = true;
}
public void Resume()
{
Paused = false;
BlockActivation = false;
}
protected override bool Process(Key key, KeyModifier modifier, KeyState state)
{
if (IsDeactivation(modifier))
{
return false;
}
if (IsActivation(key, modifier, state))
{
return true;
}
return false;
}
private bool IsActivation(Key key, KeyModifier modifier, KeyState state)
{
var changed = false;
var pressed = state == KeyState.Pressed && modifier.HasFlag(KeyModifier.Alt);
switch (key)
{
case Key.Tab:
changed = Tab != pressed;
Tab = pressed;
break;
case Key.LeftShift:
changed = LeftShift != pressed;
LeftShift = pressed;
break;
}
var isActivation = Tab && changed;
if (isActivation && !BlockActivation)
{
Activated = true;
if (LeftShift)
{
logger.Debug("Detected sequence for previous instance.");
PreviousActivated?.Invoke();
}
else
{
logger.Debug("Detected sequence for next instance.");
NextActivated?.Invoke();
}
}
return isActivation;
}
private bool IsDeactivation(KeyModifier modifier)
{
var isDeactivation = Activated && !modifier.HasFlag(KeyModifier.Alt);
if (isDeactivation)
{
Activated = false;
LeftShift = false;
Tab = false;
logger.Debug("Detected deactivation sequence.");
Deactivated?.Invoke();
}
return isDeactivation;
}
}
}