SEBWIN-567: Implemented keyboard activator for taskbar.

This commit is contained in:
Damian Büchel 2022-05-06 15:52:37 +02:00
parent 043187f4ec
commit aa6c765729
11 changed files with 199 additions and 79 deletions

View file

@ -303,6 +303,7 @@ namespace SafeExamBrowser.Client
context.Activators.Add(new ActionCenterKeyboardActivator(ModuleLogger(nameof(ActionCenterKeyboardActivator)), nativeMethods));
context.Activators.Add(new ActionCenterTouchActivator(ModuleLogger(nameof(ActionCenterTouchActivator)), nativeMethods));
context.Activators.Add(new TaskbarKeyboardActivator(ModuleLogger(nameof(TaskbarKeyboardActivator)), nativeMethods));
context.Activators.Add(new TaskviewKeyboardActivator(ModuleLogger(nameof(TaskviewKeyboardActivator)), nativeMethods));
context.Activators.Add(new TerminationActivator(ModuleLogger(nameof(TerminationActivator)), nativeMethods));

View file

@ -118,6 +118,12 @@ namespace SafeExamBrowser.Client.Operations
{
terminationActivator.Start();
}
if (Context.Settings.Taskbar.EnableTaskbar && activator is ITaskbarActivator taskbarActivator)
{
taskbar.Register(taskbarActivator);
taskbarActivator.Start();
}
}
}

View file

@ -87,6 +87,7 @@
<Compile Include="Shell\INotificationControl.cs" />
<Compile Include="Shell\ISystemControl.cs" />
<Compile Include="Shell\ITaskbar.cs" />
<Compile Include="Shell\ITaskbarActivator.cs" />
<Compile Include="Shell\ITaskview.cs" />
<Compile Include="Shell\ITaskviewActivator.cs" />
<Compile Include="Shell\ITerminationActivator.cs" />

View file

@ -57,6 +57,11 @@ namespace SafeExamBrowser.UserInterface.Contracts.Shell
/// </summary>
void Close();
/// <summary>
/// Puts the focus on the taskbar.
/// </summary>
void Focus(bool forward = true);
/// <summary>
/// Returns the absolute height of the taskbar (i.e. in physical pixels).
/// </summary>
@ -72,14 +77,14 @@ namespace SafeExamBrowser.UserInterface.Contracts.Shell
/// </summary>
void InitializeText(IText text);
/// <summary>
/// Registers the specified activator for the taskbar.
/// </summary>
void Register(ITaskbarActivator activator);
/// <summary>
/// Shows the taskbar.
/// </summary>
void Show();
/// <summary>
/// Puts the focus on the taskbar.
/// </summary>
void Focus(bool forward = true);
}
}

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using SafeExamBrowser.UserInterface.Contracts.Shell.Events;
namespace SafeExamBrowser.UserInterface.Contracts.Shell
{
/// <summary>
/// A module which can be used to activate the <see cref="ITaskbar"/>.
/// </summary>
public interface ITaskbarActivator : IActivator
{
/// <summary>
/// Fired when the taskbar should be activated (i.e. put into focus).
/// </summary>
event ActivatorEventHandler Activated;
}
}

View file

@ -79,11 +79,11 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows
internal BrowserWindow(IBrowserControl browserControl, BrowserSettings settings, bool isMainWindow, IText text, ILogger logger)
{
this.browserControl = browserControl;
this.isMainWindow = isMainWindow;
this.logger = logger;
this.settings = settings;
this.text = text;
this.logger = logger;
this.browserControl = browserControl;
InitializeComponent();
InitializeBrowserWindow(browserControl);
@ -214,12 +214,15 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows
if (e.Key == Key.Tab)
{
var hasShift = (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift;
if (Toolbar.IsKeyboardFocusWithin && hasShift)
{
var firstActiveElementInToolbar = Toolbar.PredictFocus(FocusNavigationDirection.Right);
if (firstActiveElementInToolbar is System.Windows.UIElement)
if (firstActiveElementInToolbar is UIElement)
{
var control = firstActiveElementInToolbar as System.Windows.UIElement;
var control = firstActiveElementInToolbar as UIElement;
if (control.IsKeyboardFocusWithin)
{
this.LoseFocusRequested?.Invoke(false);
@ -257,6 +260,7 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows
{
var hasCtrl = (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control;
var hasShift = (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift;
if (BrowserControlHost.IsFocused && hasCtrl)
{
if (Findbar.Visibility == Visibility.Hidden || hasShift)
@ -273,6 +277,7 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows
var focusedElement = FocusManager.GetFocusedElement(this);
var focusedControl = focusedElement as System.Windows.Controls.Control;
var prevFocusedControl = tabKeyDownFocusElement as System.Windows.Controls.Control;
if (focusedControl != null && prevFocusedControl != null)
{
//var commonAncestor = focusedControl.FindCommonVisualAncestor(prevFocusedControl);
@ -452,14 +457,14 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows
var javascript = @"
if (typeof __SEB_focusElement === 'undefined') {
__SEB_focusElement = function (forward) {
var items = [].map
.call(document.body.querySelectorAll(['input', 'select', 'a[href]', 'textarea', 'button', '[tabindex]']), function(el, i) { return { el, i } })
.filter(function(e) { return e.el.tabIndex >= 0 && !e.el.disabled && e.el.offsetParent; })
.sort(function(a,b) { return a.el.tabIndex === b.el.tabIndex ? a.i - b.i : (a.el.tabIndex || 9E9) - (b.el.tabIndex || 9E9); })
var item = items[forward ? 1 : items.length - 1];
if (item && item.focus && typeof item.focus !== 'function')
throw ('item.focus is not a function, ' + typeof item.focus)
setTimeout(function () { item && item.focus && item.focus(); }, 20);
var items = [].map
.call(document.body.querySelectorAll(['input', 'select', 'a[href]', 'textarea', 'button', '[tabindex]']), function(el, i) { return { el, i } })
.filter(function(e) { return e.el.tabIndex >= 0 && !e.el.disabled && e.el.offsetParent; })
.sort(function(a,b) { return a.el.tabIndex === b.el.tabIndex ? a.i - b.i : (a.el.tabIndex || 9E9) - (b.el.tabIndex || 9E9); })
var item = items[forward ? 1 : items.length - 1];
if (item && item.focus && typeof item.focus !== 'function')
throw ('item.focus is not a function, ' + typeof item.focus)
setTimeout(function () { item && item.focus && item.focus(); }, 20);
}
}";
this.browserControl.ExecuteJavascript(javascript, result =>
@ -593,7 +598,7 @@ if (typeof __SEB_focusElement === 'undefined') {
public void FocusToolbar(bool forward)
{
this.Dispatcher.BeginInvoke((Action)(async () =>
this.Dispatcher.BeginInvoke((Action) (async () =>
{
this.Activate();
await Task.Delay(50);
@ -613,7 +618,7 @@ if (typeof __SEB_focusElement === 'undefined') {
public void FocusBrowser()
{
this.Dispatcher.BeginInvoke((Action)(async () =>
this.Dispatcher.BeginInvoke((Action) (async () =>
{
this.FocusToolbar(false);
await Task.Delay(100);
@ -630,7 +635,7 @@ if (typeof __SEB_focusElement === 'undefined') {
public void FocusAddressBar()
{
this.Dispatcher.BeginInvoke((Action)(async () =>
this.Dispatcher.BeginInvoke((Action) (() =>
{
this.UrlTextBox.Focus();
}));

View file

@ -20,8 +20,9 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows
{
internal partial class Taskbar : Window, ITaskbar
{
private readonly ILogger logger;
private bool allowClose;
private ILogger logger;
private bool isQuitButtonFocusedAtKeyDown;
private bool isFirstChildFocusedAtKeyDown;
@ -82,6 +83,23 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows
Dispatcher.Invoke(base.Close);
}
public void Focus(bool forward)
{
Dispatcher.BeginInvoke((Action) (() =>
{
Activate();
if (forward)
{
SetFocusWithin(ApplicationStackPanel.Children[0]);
}
else
{
QuitButton.Focus();
}
}));
}
public int GetAbsoluteHeight()
{
return Dispatcher.Invoke(() =>
@ -121,6 +139,13 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows
});
}
private void InitializeTaskbar()
{
Closing += Taskbar_Closing;
Loaded += (o, args) => InitializeBounds();
QuitButton.Clicked += QuitButton_Clicked;
}
public void InitializeText(IText text)
{
Dispatcher.Invoke(() =>
@ -131,11 +156,21 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows
});
}
public void Register(ITaskbarActivator activator)
{
activator.Activated += Activator_Activated;
}
public new void Show()
{
Dispatcher.Invoke(base.Show);
}
private void Activator_Activated()
{
(this as ITaskbar).Focus(true);
}
private void QuitButton_Clicked(CancelEventArgs args)
{
QuitButtonClicked?.Invoke(args);
@ -160,17 +195,10 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows
}
}
private void InitializeTaskbar()
{
Closing += Taskbar_Closing;
Loaded += (o, args) => InitializeBounds();
QuitButton.Clicked += QuitButton_Clicked;
}
private void Window_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
this.isQuitButtonFocusedAtKeyDown = this.QuitButton.IsKeyboardFocusWithin;
this.isFirstChildFocusedAtKeyDown = this.ApplicationStackPanel.Children[0].IsKeyboardFocusWithin;
isQuitButtonFocusedAtKeyDown = QuitButton.IsKeyboardFocusWithin;
isFirstChildFocusedAtKeyDown = ApplicationStackPanel.Children[0].IsKeyboardFocusWithin;
}
private void Window_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
@ -178,36 +206,20 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows
if (e.Key == System.Windows.Input.Key.Tab)
{
var shift = System.Windows.Input.Keyboard.IsKeyDown(System.Windows.Input.Key.LeftShift);
if (!shift && this.ApplicationStackPanel.Children[0].IsKeyboardFocusWithin && this.isQuitButtonFocusedAtKeyDown)
if (!shift && ApplicationStackPanel.Children[0].IsKeyboardFocusWithin && isQuitButtonFocusedAtKeyDown)
{
this.LoseFocusRequested?.Invoke(true);
LoseFocusRequested?.Invoke(true);
e.Handled = true;
}
else if (shift && this.QuitButton.IsKeyboardFocusWithin && this.isFirstChildFocusedAtKeyDown)
else if (shift && QuitButton.IsKeyboardFocusWithin && isFirstChildFocusedAtKeyDown)
{
this.LoseFocusRequested?.Invoke(false);
LoseFocusRequested?.Invoke(false);
e.Handled = true;
}
}
this.isQuitButtonFocusedAtKeyDown = false;
this.isFirstChildFocusedAtKeyDown = false;
}
void ITaskbar.Focus(bool forward)
{
this.Dispatcher.BeginInvoke((Action)(() =>
{
base.Activate();
if (forward)
{
this.SetFocusWithin(this.ApplicationStackPanel.Children[0]);
}
else
{
this.QuitButton.Focus();
}
}));
isQuitButtonFocusedAtKeyDown = false;
isFirstChildFocusedAtKeyDown = false;
}
private bool SetFocusWithin(UIElement uIElement)
@ -215,15 +227,17 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows
if (uIElement.Focusable)
{
uIElement.Focus();
return true;
}
if (uIElement is System.Windows.Controls.Panel)
{
var panel = uIElement as System.Windows.Controls.Panel;
for (var i = 0; i < panel.Children.Count; i++)
{
if (this.SetFocusWithin(panel.Children[i]))
if (SetFocusWithin(panel.Children[i]))
{
return true;
}
@ -235,9 +249,10 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows
{
var control = uIElement as System.Windows.Controls.ContentControl;
var content = control.Content as UIElement;
if (content != null)
{
return this.SetFocusWithin(content);
return SetFocusWithin(content);
}
}

View file

@ -33,12 +33,12 @@ namespace SafeExamBrowser.UserInterface.Mobile.Windows
internal partial class BrowserWindow : Window, IBrowserWindow
{
private readonly bool isMainWindow;
private readonly ILogger logger;
private readonly BrowserSettings settings;
private readonly IText text;
private WindowClosedEventHandler closed;
private WindowClosingEventHandler closing;
private ILogger logger;
private WindowSettings WindowSettings
{
@ -54,7 +54,7 @@ namespace SafeExamBrowser.UserInterface.Mobile.Windows
public event ActionRequestedEventHandler DeveloperConsoleRequested;
public event FindRequestedEventHandler FindRequested;
public event ActionRequestedEventHandler ForwardNavigationRequested;
public event LoseFocusRequestedEventHandler LoseFocusRequested;
public event LoseFocusRequestedEventHandler LoseFocusRequested { add { } remove { } }
public event ActionRequestedEventHandler HomeNavigationRequested;
public event ActionRequestedEventHandler ReloadRequested;
public event ActionRequestedEventHandler ZoomInRequested;
@ -76,9 +76,9 @@ namespace SafeExamBrowser.UserInterface.Mobile.Windows
internal BrowserWindow(IBrowserControl browserControl, BrowserSettings settings, bool isMainWindow, IText text, ILogger logger)
{
this.isMainWindow = isMainWindow;
this.logger = logger;
this.settings = settings;
this.text = text;
this.logger = logger;
InitializeComponent();
InitializeBrowserWindow(browserControl);

View file

@ -19,8 +19,8 @@ namespace SafeExamBrowser.UserInterface.Mobile.Windows
{
internal partial class Taskbar : Window, ITaskbar
{
private readonly ILogger logger;
private bool allowClose;
private ILogger logger;
public bool ShowClock
{
@ -32,7 +32,7 @@ namespace SafeExamBrowser.UserInterface.Mobile.Windows
set { Dispatcher.Invoke(() => QuitButton.Visibility = value ? Visibility.Visible : Visibility.Collapsed); }
}
public event LoseFocusRequestedEventHandler LoseFocusRequested;
public event LoseFocusRequestedEventHandler LoseFocusRequested { add { } remove { } }
public event QuitButtonClickedEventHandler QuitButtonClicked;
internal Taskbar(ILogger logger)
@ -79,6 +79,20 @@ namespace SafeExamBrowser.UserInterface.Mobile.Windows
Dispatcher.Invoke(base.Close);
}
public void Focus(bool fromTop)
{
Activate();
if (fromTop)
{
ApplicationStackPanel.Children[0].Focus();
}
else
{
QuitButton.Focus();
}
}
public int GetAbsoluteHeight()
{
return Dispatcher.Invoke(() =>
@ -118,6 +132,13 @@ namespace SafeExamBrowser.UserInterface.Mobile.Windows
});
}
private void InitializeTaskbar()
{
Closing += Taskbar_Closing;
Loaded += (o, args) => InitializeBounds();
QuitButton.Clicked += QuitButton_Clicked;
}
public void InitializeText(IText text)
{
Dispatcher.Invoke(() =>
@ -126,11 +147,21 @@ namespace SafeExamBrowser.UserInterface.Mobile.Windows
});
}
public void Register(ITaskbarActivator activator)
{
activator.Activated += Activator_Activated;
}
public new void Show()
{
Dispatcher.Invoke(base.Show);
}
private void Activator_Activated()
{
(this as ITaskbar).Focus(true);
}
private void QuitButton_Clicked(CancelEventArgs args)
{
QuitButtonClicked?.Invoke(args);
@ -154,25 +185,5 @@ namespace SafeExamBrowser.UserInterface.Mobile.Windows
e.Cancel = true;
}
}
private void InitializeTaskbar()
{
Closing += Taskbar_Closing;
Loaded += (o, args) => InitializeBounds();
QuitButton.Clicked += QuitButton_Clicked;
}
void ITaskbar.Focus(bool fromTop)
{
base.Activate();
if (fromTop)
{
this.ApplicationStackPanel.Children[0].Focus();
}
else
{
this.QuitButton.Focus();
}
}
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2022 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Windows.Input;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Shell;
using SafeExamBrowser.UserInterface.Contracts.Shell.Events;
using SafeExamBrowser.WindowsApi.Contracts;
using SafeExamBrowser.WindowsApi.Contracts.Events;
namespace SafeExamBrowser.UserInterface.Shared.Activators
{
public class TaskbarKeyboardActivator : KeyboardActivator, ITaskbarActivator
{
private readonly ILogger logger;
private bool leftWindows;
public event ActivatorEventHandler Activated;
public TaskbarKeyboardActivator(ILogger logger, INativeMethods nativeMethods) : base(nativeMethods)
{
this.logger = logger;
}
protected override bool Process(Key key, KeyModifier modifier, KeyState state)
{
var changed = false;
var pressed = state == KeyState.Pressed;
if (key == Key.LWin)
{
changed = leftWindows != pressed;
leftWindows = pressed;
}
if (leftWindows && changed)
{
logger.Debug("Detected activation sequence for taskbar.");
Activated?.Invoke();
return true;
}
return false;
}
}
}

View file

@ -66,6 +66,7 @@
<Compile Include="Activators\ActionCenterKeyboardActivator.cs" />
<Compile Include="Activators\ActionCenterTouchActivator.cs" />
<Compile Include="Activators\KeyboardActivator.cs" />
<Compile Include="Activators\TaskbarKeyboardActivator.cs" />
<Compile Include="Activators\TaskviewKeyboardActivator.cs" />
<Compile Include="Activators\TerminationActivator.cs" />
<Compile Include="Activators\TouchActivator.cs" />