SEBWIN-312: Started implementing task view.
This commit is contained in:
parent
5f31656649
commit
08bf49b61b
19 changed files with 248 additions and 33 deletions
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue