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> /// <summary>
/// The resource providing the application icon. /// The resource providing the application icon.
/// </summary> /// </summary>
public IconResource IconResource { get; set; } public IconResource Icon { get; set; }
} }
} }

View file

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

View file

@ -64,7 +64,7 @@ namespace SafeExamBrowser.Applications
private IApplication BuildApplication(string executablePath, WhitelistApplication settings) private IApplication BuildApplication(string executablePath, WhitelistApplication settings)
{ {
var icon = new IconResource { Type = IconResourceType.Embedded, Uri = new Uri(executablePath) }; 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); var application = new ExternalApplication(executablePath, info, logger.CloneFor(settings.DisplayName), processFactory);
return application; return application;

View file

@ -44,9 +44,11 @@ namespace SafeExamBrowser.Applications
{ {
logger.Info("Starting application..."); logger.Info("Starting application...");
// TODO: Ensure that SEB does not crash if an application cannot be started!!
var process = processFactory.StartNew(executablePath); var process = processFactory.StartNew(executablePath);
var id = new ApplicationInstanceIdentifier(process.Id); 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(); instance.Initialize();
instances.Add(instance); instances.Add(instance);

View file

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

View file

@ -65,7 +65,7 @@ namespace SafeExamBrowser.Browser
var cefSettings = InitializeCefSettings(); var cefSettings = InitializeCefSettings();
var success = Cef.Initialize(cefSettings, true, default(IApp)); var success = Cef.Initialize(cefSettings, true, default(IApp));
Info = BuildApplicationInfo(); InitializeApplicationInfo();
if (success) if (success)
{ {
@ -95,11 +95,11 @@ namespace SafeExamBrowser.Browser
logger.Info("Terminated 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", Name = "Safe Exam Browser",
Tooltip = text.Get(TextKey.Browser_Tooltip) Tooltip = text.Get(TextKey.Browser_Tooltip)
}; };

View file

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

View file

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

View file

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

View file

@ -16,13 +16,18 @@ namespace SafeExamBrowser.UserInterface.Contracts.Shell
public interface ITaskViewActivator : IActivator public interface ITaskViewActivator : IActivator
{ {
/// <summary> /// <summary>
/// Fired when the next application instance should be selected. /// Fired when the task view should be hidden.
/// </summary> /// </summary>
event ActivatorEventHandler Next; event ActivatorEventHandler Deactivated;
/// <summary> /// <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> /// </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() private void InitializeApplicationInstanceButton()
{ {
Icon.Content = IconResourceLoader.Load(info.IconResource); Icon.Content = IconResourceLoader.Load(info.Icon);
Text.Text = instance?.Name ?? info.Name; Text.Text = instance?.Name ?? info.Name;
Button.Click += (o, args) => Clicked?.Invoke(this, EventArgs.Empty); Button.Click += (o, args) => Clicked?.Invoke(this, EventArgs.Empty);
Button.ToolTip = instance?.Name ?? info.Tooltip; Button.ToolTip = instance?.Name ?? info.Tooltip;

View file

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

View file

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

View file

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

View file

@ -6,27 +6,156 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * 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;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using SafeExamBrowser.Applications.Contracts; using SafeExamBrowser.Applications.Contracts;
using SafeExamBrowser.Core.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Shell; using SafeExamBrowser.UserInterface.Contracts.Shell;
using SafeExamBrowser.UserInterface.Shared.Utilities;
namespace SafeExamBrowser.UserInterface.Desktop namespace SafeExamBrowser.UserInterface.Desktop
{ {
public partial class TaskView : Window, ITaskView public partial class TaskView : Window, ITaskView
{ {
private IList<IApplicationInstance> instances;
public TaskView() public TaskView()
{ {
InitializeComponent(); InitializeComponent();
instances = new List<IApplicationInstance>();
} }
public void Add(IApplication application) public void Add(IApplication application)
{ {
application.InstanceStarted += Application_InstanceStarted;
} }
public void Register(ITaskViewActivator activator) 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() private void InitializeApplicationInstanceButton()
{ {
Icon.Content = IconResourceLoader.Load(info.IconResource); Icon.Content = IconResourceLoader.Load(info.Icon);
Text.Text = instance?.Name ?? info.Name; Text.Text = instance?.Name ?? info.Name;
Button.Click += (o, args) => Clicked?.Invoke(this, EventArgs.Empty); Button.Click += (o, args) => Clicked?.Invoke(this, EventArgs.Empty);
Button.ToolTip = instance?.Name ?? info.Tooltip; Button.ToolTip = instance?.Name ?? info.Tooltip;

View file

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

View file

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

View file

@ -17,10 +17,12 @@ namespace SafeExamBrowser.UserInterface.Shared.Activators
{ {
public class TaskViewKeyboardActivator : KeyboardActivator, ITaskViewActivator public class TaskViewKeyboardActivator : KeyboardActivator, ITaskViewActivator
{ {
private bool Activated, BlockActivation, LeftShift, Tab;
private ILogger logger; private ILogger logger;
public event ActivatorEventHandler Next; public event ActivatorEventHandler Deactivated;
public event ActivatorEventHandler Previous; public event ActivatorEventHandler NextActivated;
public event ActivatorEventHandler PreviousActivated;
public TaskViewKeyboardActivator(ILogger logger, INativeMethods nativeMethods) : base(nativeMethods) public TaskViewKeyboardActivator(ILogger logger, INativeMethods nativeMethods) : base(nativeMethods)
{ {
@ -29,17 +31,82 @@ namespace SafeExamBrowser.UserInterface.Shared.Activators
public void Pause() public void Pause()
{ {
Paused = true; BlockActivation = true;
} }
public void Resume() public void Resume()
{ {
Paused = false; BlockActivation = false;
} }
protected override bool Process(Key key, KeyModifier modifier, KeyState state) protected override bool Process(Key key, KeyModifier modifier, KeyState state)
{ {
if (IsDeactivation(modifier))
{
return false;
}
if (IsActivation(key, modifier, state))
{
return true;
}
return false; 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;
}
} }
} }