diff --git a/SafeExamBrowser.Client/CompositionRoot.cs b/SafeExamBrowser.Client/CompositionRoot.cs index 86902dba..3b6d191a 100644 --- a/SafeExamBrowser.Client/CompositionRoot.cs +++ b/SafeExamBrowser.Client/CompositionRoot.cs @@ -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)); diff --git a/SafeExamBrowser.Client/Operations/ShellOperation.cs b/SafeExamBrowser.Client/Operations/ShellOperation.cs index c548b618..76a64bd4 100644 --- a/SafeExamBrowser.Client/Operations/ShellOperation.cs +++ b/SafeExamBrowser.Client/Operations/ShellOperation.cs @@ -118,6 +118,12 @@ namespace SafeExamBrowser.Client.Operations { terminationActivator.Start(); } + + if (Context.Settings.Taskbar.EnableTaskbar && activator is ITaskbarActivator taskbarActivator) + { + taskbar.Register(taskbarActivator); + taskbarActivator.Start(); + } } } diff --git a/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj b/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj index 5eb5ab83..8e05824e 100644 --- a/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj +++ b/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj @@ -87,6 +87,7 @@ + diff --git a/SafeExamBrowser.UserInterface.Contracts/Shell/ITaskbar.cs b/SafeExamBrowser.UserInterface.Contracts/Shell/ITaskbar.cs index 0dd67e8a..1a22cb12 100644 --- a/SafeExamBrowser.UserInterface.Contracts/Shell/ITaskbar.cs +++ b/SafeExamBrowser.UserInterface.Contracts/Shell/ITaskbar.cs @@ -57,6 +57,11 @@ namespace SafeExamBrowser.UserInterface.Contracts.Shell /// void Close(); + /// + /// Puts the focus on the taskbar. + /// + void Focus(bool forward = true); + /// /// Returns the absolute height of the taskbar (i.e. in physical pixels). /// @@ -72,14 +77,14 @@ namespace SafeExamBrowser.UserInterface.Contracts.Shell /// void InitializeText(IText text); + /// + /// Registers the specified activator for the taskbar. + /// + void Register(ITaskbarActivator activator); + /// /// Shows the taskbar. /// void Show(); - - /// - /// Puts the focus on the taskbar. - /// - void Focus(bool forward = true); } } diff --git a/SafeExamBrowser.UserInterface.Contracts/Shell/ITaskbarActivator.cs b/SafeExamBrowser.UserInterface.Contracts/Shell/ITaskbarActivator.cs new file mode 100644 index 00000000..4a9fd222 --- /dev/null +++ b/SafeExamBrowser.UserInterface.Contracts/Shell/ITaskbarActivator.cs @@ -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 +{ + /// + /// A module which can be used to activate the . + /// + public interface ITaskbarActivator : IActivator + { + /// + /// Fired when the taskbar should be activated (i.e. put into focus). + /// + event ActivatorEventHandler Activated; + } +} diff --git a/SafeExamBrowser.UserInterface.Desktop/Windows/BrowserWindow.xaml.cs b/SafeExamBrowser.UserInterface.Desktop/Windows/BrowserWindow.xaml.cs index 2d4f4126..fbde4baf 100644 --- a/SafeExamBrowser.UserInterface.Desktop/Windows/BrowserWindow.xaml.cs +++ b/SafeExamBrowser.UserInterface.Desktop/Windows/BrowserWindow.xaml.cs @@ -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(); })); diff --git a/SafeExamBrowser.UserInterface.Desktop/Windows/Taskbar.xaml.cs b/SafeExamBrowser.UserInterface.Desktop/Windows/Taskbar.xaml.cs index f4a184d2..78ad173b 100644 --- a/SafeExamBrowser.UserInterface.Desktop/Windows/Taskbar.xaml.cs +++ b/SafeExamBrowser.UserInterface.Desktop/Windows/Taskbar.xaml.cs @@ -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); } } diff --git a/SafeExamBrowser.UserInterface.Mobile/Windows/BrowserWindow.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Windows/BrowserWindow.xaml.cs index 8110d4d9..5569fb06 100644 --- a/SafeExamBrowser.UserInterface.Mobile/Windows/BrowserWindow.xaml.cs +++ b/SafeExamBrowser.UserInterface.Mobile/Windows/BrowserWindow.xaml.cs @@ -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); diff --git a/SafeExamBrowser.UserInterface.Mobile/Windows/Taskbar.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Windows/Taskbar.xaml.cs index 9ddfee07..beeaff73 100644 --- a/SafeExamBrowser.UserInterface.Mobile/Windows/Taskbar.xaml.cs +++ b/SafeExamBrowser.UserInterface.Mobile/Windows/Taskbar.xaml.cs @@ -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(); - } - } } } diff --git a/SafeExamBrowser.UserInterface.Shared/Activators/TaskbarKeyboardActivator.cs b/SafeExamBrowser.UserInterface.Shared/Activators/TaskbarKeyboardActivator.cs new file mode 100644 index 00000000..3bf8e743 --- /dev/null +++ b/SafeExamBrowser.UserInterface.Shared/Activators/TaskbarKeyboardActivator.cs @@ -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; + } + } +} diff --git a/SafeExamBrowser.UserInterface.Shared/SafeExamBrowser.UserInterface.Shared.csproj b/SafeExamBrowser.UserInterface.Shared/SafeExamBrowser.UserInterface.Shared.csproj index 3ea7e132..064fbe30 100644 --- a/SafeExamBrowser.UserInterface.Shared/SafeExamBrowser.UserInterface.Shared.csproj +++ b/SafeExamBrowser.UserInterface.Shared/SafeExamBrowser.UserInterface.Shared.csproj @@ -66,6 +66,7 @@ +