diff --git a/SafeExamBrowser.Browser.Contracts/Events/LoseFocusRequestedEventHandler.cs b/SafeExamBrowser.Browser.Contracts/Events/LoseFocusRequestedEventHandler.cs new file mode 100644 index 00000000..56af93ac --- /dev/null +++ b/SafeExamBrowser.Browser.Contracts/Events/LoseFocusRequestedEventHandler.cs @@ -0,0 +1,15 @@ +/* + * 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/. + */ + +namespace SafeExamBrowser.Browser.Contracts.Events +{ + /// + /// Event handler used to indicate that the user wants to move the focus away from the item. + /// + public delegate void LoseFocusRequestedEventHandler(bool forward); +} diff --git a/SafeExamBrowser.Browser.Contracts/Events/TabPressedEventHandler.cs b/SafeExamBrowser.Browser.Contracts/Events/TabPressedEventHandler.cs new file mode 100644 index 00000000..f9586a6c --- /dev/null +++ b/SafeExamBrowser.Browser.Contracts/Events/TabPressedEventHandler.cs @@ -0,0 +1,15 @@ +/* + * 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/. + */ + +namespace SafeExamBrowser.Browser.Contracts.Events +{ + /// + /// Event handler used to indicate that the user pressed the tab key to move the focus forward or backward. + /// + public delegate void TabPressedEventHandler(bool forward); +} diff --git a/SafeExamBrowser.Browser.Contracts/IBrowserApplication.cs b/SafeExamBrowser.Browser.Contracts/IBrowserApplication.cs index d89f9b57..e1d9df7a 100644 --- a/SafeExamBrowser.Browser.Contracts/IBrowserApplication.cs +++ b/SafeExamBrowser.Browser.Contracts/IBrowserApplication.cs @@ -6,6 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +using System; using SafeExamBrowser.Applications.Contracts; using SafeExamBrowser.Browser.Contracts.Events; @@ -30,5 +31,16 @@ namespace SafeExamBrowser.Browser.Contracts /// Event fired when the browser application detects a request to terminate SEB. /// event TerminationRequestedEventHandler TerminationRequested; + + /// + /// Event fired when the user tries to focus the taskbar. + /// + event LoseFocusRequestedEventHandler LoseFocusRequested; + + /// + /// Transfers the focus to the browser window. + /// If true, the first focusable element in the browser window receives focus (passing forward of focus). Otherwise, the last element. + /// + void Focus(bool forward); } } diff --git a/SafeExamBrowser.Browser.Contracts/SafeExamBrowser.Browser.Contracts.csproj b/SafeExamBrowser.Browser.Contracts/SafeExamBrowser.Browser.Contracts.csproj index 542024a9..02460564 100644 --- a/SafeExamBrowser.Browser.Contracts/SafeExamBrowser.Browser.Contracts.csproj +++ b/SafeExamBrowser.Browser.Contracts/SafeExamBrowser.Browser.Contracts.csproj @@ -57,6 +57,8 @@ + + diff --git a/SafeExamBrowser.Browser/BrowserApplication.cs b/SafeExamBrowser.Browser/BrowserApplication.cs index c98f4924..9f2504dc 100644 --- a/SafeExamBrowser.Browser/BrowserApplication.cs +++ b/SafeExamBrowser.Browser/BrowserApplication.cs @@ -58,6 +58,7 @@ namespace SafeExamBrowser.Browser public event DownloadRequestedEventHandler ConfigurationDownloadRequested; public event SessionIdentifierDetectedEventHandler SessionIdentifierDetected; + public event LoseFocusRequestedEventHandler LoseFocusRequested; public event TerminationRequestedEventHandler TerminationRequested; public event WindowsChangedEventHandler WindowsChanged; @@ -195,6 +196,7 @@ namespace SafeExamBrowser.Browser window.ResetRequested += Window_ResetRequested; window.SessionIdentifierDetected += (i) => SessionIdentifierDetected?.Invoke(i); window.TerminationRequested += () => TerminationRequested?.Invoke(); + window.LoseFocusRequested += (forward) => LoseFocusRequested?.Invoke(forward); window.InitializeControl(); windows.Add(window); @@ -457,5 +459,13 @@ namespace SafeExamBrowser.Browser CreateNewWindow(); logger.Info("Successfully reset browser."); } + + public void Focus(bool forward) + { + windows.ForEach(window => + { + window.Focus(forward); + }); + } } } diff --git a/SafeExamBrowser.Browser/BrowserControl.cs b/SafeExamBrowser.Browser/BrowserControl.cs index e9635c6c..3b569d43 100644 --- a/SafeExamBrowser.Browser/BrowserControl.cs +++ b/SafeExamBrowser.Browser/BrowserControl.cs @@ -15,6 +15,7 @@ using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.Configuration.Contracts.Cryptography; using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.UserInterface.Contracts.Browser; +using SafeExamBrowser.UserInterface.Contracts.Browser.Data; using SafeExamBrowser.UserInterface.Contracts.Browser.Events; namespace SafeExamBrowser.Browser @@ -143,5 +144,19 @@ namespace SafeExamBrowser.Browser { control.BrowserCore.SetZoomLevel(level); } + + /// + /// Executes the given Javascript code in the browser. + /// + public async void ExecuteJavascript(string javascript, Action callback) + { + var result = await this.control.EvaluateScriptAsync(javascript); + callback(new JavascriptResult() + { + Message = result.Message, + Result = result.Result, + Success = result.Success + }); + } } } diff --git a/SafeExamBrowser.Browser/BrowserWindow.cs b/SafeExamBrowser.Browser/BrowserWindow.cs index e008f31c..89a3823b 100644 --- a/SafeExamBrowser.Browser/BrowserWindow.cs +++ b/SafeExamBrowser.Browser/BrowserWindow.cs @@ -80,6 +80,7 @@ namespace SafeExamBrowser.Browser internal event PopupRequestedEventHandler PopupRequested; internal event ResetRequestedEventHandler ResetRequested; internal event SessionIdentifierDetectedEventHandler SessionIdentifierDetected; + internal event LoseFocusRequestedEventHandler LoseFocusRequested; internal event TerminationRequestedEventHandler TerminationRequested; public event IconChangedEventHandler IconChanged; @@ -162,6 +163,8 @@ namespace SafeExamBrowser.Browser keyboardHandler.ZoomInRequested += ZoomInRequested; keyboardHandler.ZoomOutRequested += ZoomOutRequested; keyboardHandler.ZoomResetRequested += ZoomResetRequested; + keyboardHandler.TabPressed += TabPressed; + keyboardHandler.FocusAddressBarRequested += FocusAddressBarRequested; resourceHandler.SessionIdentifierDetected += (id) => SessionIdentifierDetected?.Invoke(id); requestHandler.QuitUrlVisited += RequestHandler_QuitUrlVisited; requestHandler.RequestBlocked += RequestHandler_RequestBlocked; @@ -189,7 +192,7 @@ namespace SafeExamBrowser.Browser internal void InitializeWindow() { - window = uiFactory.CreateBrowserWindow(Control, settings, isMainWindow); + window = uiFactory.CreateBrowserWindow(Control, settings, isMainWindow, this.logger); window.AddressChanged += Window_AddressChanged; window.BackwardNavigationRequested += Window_BackwardNavigationRequested; window.Closed += Window_Closed; @@ -198,6 +201,7 @@ namespace SafeExamBrowser.Browser window.FindRequested += Window_FindRequested; window.ForwardNavigationRequested += Window_ForwardNavigationRequested; window.HomeNavigationRequested += HomeNavigationRequested; + window.LoseFocusRequested += Window_LoseFocusRequested; window.ReloadRequested += ReloadRequested; window.ZoomInRequested += ZoomInRequested; window.ZoomOutRequested += ZoomOutRequested; @@ -454,6 +458,11 @@ namespace SafeExamBrowser.Browser } } + private void FocusAddressBarRequested() + { + window.FocusAddressBar(); + } + private ChromiumHostControl LifeSpanHandler_CreatePopup() { var args = new PopupRequestedEventArgs(); @@ -655,6 +664,11 @@ namespace SafeExamBrowser.Browser Control.NavigateForwards(); } + private void Window_LoseFocusRequested(bool forward) + { + LoseFocusRequested?.Invoke(forward); + } + private void ZoomInRequested() { if (settings.AllowPageZoom && CalculateZoomPercentage() < 300) @@ -688,6 +702,43 @@ namespace SafeExamBrowser.Browser } } + private void TabPressed(bool shiftPressed) + { + this.Control.ExecuteJavascript("document.activeElement.tagName", result => + { + var tagName = result.Result as string; + if (tagName != null) + { + if (tagName.ToUpper() == "BODY") + { + // this means the user is now at the start of the focus / tabIndex chain in the website + if (shiftPressed) + { + window.FocusToolbar(!shiftPressed); + } + else + { + this.LoseFocusRequested?.Invoke(true); + } + } + } + }); + } + + internal void Focus(bool forward) + { + if (forward) + { + window.FocusToolbar(forward); + } + else + { + window.FocusBrowser(); + + this.Activate(); + } + } + private double CalculateZoomPercentage() { return (zoomLevel * 25.0) + 100.0; diff --git a/SafeExamBrowser.Browser/Handlers/KeyboardHandler.cs b/SafeExamBrowser.Browser/Handlers/KeyboardHandler.cs index ec54b60d..aae93188 100644 --- a/SafeExamBrowser.Browser/Handlers/KeyboardHandler.cs +++ b/SafeExamBrowser.Browser/Handlers/KeyboardHandler.cs @@ -8,6 +8,7 @@ using System.Windows.Forms; using CefSharp; +using SafeExamBrowser.Browser.Contracts.Events; using SafeExamBrowser.UserInterface.Contracts; namespace SafeExamBrowser.Browser.Handlers @@ -20,6 +21,10 @@ namespace SafeExamBrowser.Browser.Handlers internal event ActionRequestedEventHandler ZoomInRequested; internal event ActionRequestedEventHandler ZoomOutRequested; internal event ActionRequestedEventHandler ZoomResetRequested; + internal event ActionRequestedEventHandler FocusAddressBarRequested; + internal event TabPressedEventHandler TabPressed; + + private int? currentKeyDown = null; public bool OnKeyEvent(IWebBrowser browserControl, IBrowser browser, KeyType type, int keyCode, int nativeKeyCode, CefEventFlags modifiers, bool isSystemKey) { @@ -38,6 +43,11 @@ namespace SafeExamBrowser.Browser.Handlers HomeNavigationRequested?.Invoke(); } + if (ctrl && keyCode == (int) Keys.L) + { + FocusAddressBarRequested?.Invoke(); + } + if ((ctrl && keyCode == (int) Keys.Add) || (ctrl && keyCode == (int) Keys.Oemplus) || (ctrl && shift && keyCode == (int) Keys.D1)) { ZoomInRequested?.Invoke(); @@ -52,8 +62,14 @@ namespace SafeExamBrowser.Browser.Handlers { ZoomResetRequested?.Invoke(); } + + if (keyCode == (int)Keys.Tab && keyCode == currentKeyDown) + { + TabPressed?.Invoke(shift); + } } + currentKeyDown = null; return false; } @@ -66,6 +82,11 @@ namespace SafeExamBrowser.Browser.Handlers return true; } + if (type == KeyType.RawKeyDown || type == KeyType.KeyDown) + { + currentKeyDown = keyCode; + } + return false; } } diff --git a/SafeExamBrowser.Client/ClientController.cs b/SafeExamBrowser.Client/ClientController.cs index 1a59d1e9..d5573a5d 100644 --- a/SafeExamBrowser.Client/ClientController.cs +++ b/SafeExamBrowser.Client/ClientController.cs @@ -188,6 +188,7 @@ namespace SafeExamBrowser.Client Browser.ConfigurationDownloadRequested += Browser_ConfigurationDownloadRequested; Browser.SessionIdentifierDetected += Browser_SessionIdentifierDetected; Browser.TerminationRequested += Browser_TerminationRequested; + Browser.LoseFocusRequested += Browser_LoseFocusRequested; ClientHost.ExamSelectionRequested += ClientHost_ExamSelectionRequested; ClientHost.MessageBoxRequested += ClientHost_MessageBoxRequested; ClientHost.PasswordRequested += ClientHost_PasswordRequested; @@ -198,6 +199,7 @@ namespace SafeExamBrowser.Client displayMonitor.DisplayChanged += DisplayMonitor_DisplaySettingsChanged; runtime.ConnectionLost += Runtime_ConnectionLost; systemMonitor.SessionSwitched += SystemMonitor_SessionSwitched; + taskbar.LoseFocusRequested += Taskbar_LoseFocusRequested; taskbar.QuitButtonClicked += Shell_QuitButtonClicked; foreach (var activator in context.Activators.OfType()) @@ -211,6 +213,16 @@ namespace SafeExamBrowser.Client } } + private void Taskbar_LoseFocusRequested(bool forward) + { + this.Browser.Focus(forward); + } + + private void Browser_LoseFocusRequested(bool forward) + { + this.taskbar.Focus(forward); + } + private void DeregisterEvents() { actionCenter.QuitButtonClicked -= Shell_QuitButtonClicked; @@ -226,6 +238,7 @@ namespace SafeExamBrowser.Client Browser.ConfigurationDownloadRequested -= Browser_ConfigurationDownloadRequested; Browser.SessionIdentifierDetected -= Browser_SessionIdentifierDetected; Browser.TerminationRequested -= Browser_TerminationRequested; + Browser.LoseFocusRequested -= Browser_LoseFocusRequested; } if (ClientHost != null) diff --git a/SafeExamBrowser.I18n.Contracts/TextKey.cs b/SafeExamBrowser.I18n.Contracts/TextKey.cs index 5c9271d0..b2ce756d 100644 --- a/SafeExamBrowser.I18n.Contracts/TextKey.cs +++ b/SafeExamBrowser.I18n.Contracts/TextKey.cs @@ -24,12 +24,19 @@ namespace SafeExamBrowser.I18n.Contracts Browser_LoadErrorTitle, Browser_Name, Browser_Tooltip, + BrowserWindow_BackwardButton, BrowserWindow_DeveloperConsoleMenuItem, BrowserWindow_Downloading, BrowserWindow_DownloadCancelled, BrowserWindow_DownloadComplete, + BrowserWindow_DownloadsButton, BrowserWindow_FindCaseSensitive, BrowserWindow_FindMenuItem, + BrowserWindow_ForwardButton, + BrowserWindow_HomeButton, + BrowserWindow_MenuButton, + BrowserWindow_ReloadButton, + BrowserWindow_UrlTextBox, BrowserWindow_ZoomMenuItem, Build, ExamSelectionDialog_Cancel, diff --git a/SafeExamBrowser.I18n/Data/de.xml b/SafeExamBrowser.I18n/Data/de.xml index fb6d4cbc..f9f905d8 100644 --- a/SafeExamBrowser.I18n/Data/de.xml +++ b/SafeExamBrowser.I18n/Data/de.xml @@ -48,6 +48,27 @@ Seite durchsuchen... + + Neu laden + + + Rückwärts + + + Vorwärts + + + Download + + + Home + + + Menü + + + URL eingeben + Seiten-Zoom diff --git a/SafeExamBrowser.I18n/Data/en.xml b/SafeExamBrowser.I18n/Data/en.xml index ab2829f5..00fb5b46 100644 --- a/SafeExamBrowser.I18n/Data/en.xml +++ b/SafeExamBrowser.I18n/Data/en.xml @@ -48,6 +48,27 @@ Search page... + + Reload + + + Backward + + + Forward + + + Download + + + Home + + + Menu + + + Enter URL + Page Zoom diff --git a/SafeExamBrowser.I18n/Data/fr.xml b/SafeExamBrowser.I18n/Data/fr.xml index 37bbf95b..f2f89c86 100644 --- a/SafeExamBrowser.I18n/Data/fr.xml +++ b/SafeExamBrowser.I18n/Data/fr.xml @@ -48,6 +48,27 @@ Rechercher dans la page... + + Recharger + + + En arrière + + + Avant + + + Télécharger + + + Accueil + + + Menu + + + Entrer l'URL + Zoom de la page diff --git a/SafeExamBrowser.I18n/Data/it.xml b/SafeExamBrowser.I18n/Data/it.xml index 9fb9d6fd..b99ba6a1 100644 --- a/SafeExamBrowser.I18n/Data/it.xml +++ b/SafeExamBrowser.I18n/Data/it.xml @@ -48,6 +48,27 @@ Pagina di ricerca ... + + Ricarica + + + Indietro + + + Avanti + + + Scaricare + + + Home + + + Menu + + + Inserisci URL + Zoom della pagina diff --git a/SafeExamBrowser.I18n/Data/zh.xml b/SafeExamBrowser.I18n/Data/zh.xml index 653d2e4c..87076aa3 100644 --- a/SafeExamBrowser.I18n/Data/zh.xml +++ b/SafeExamBrowser.I18n/Data/zh.xml @@ -45,6 +45,27 @@ 下载完成 + + 重新加载 + + + 向后 + + + 前进 + + + 下载 + + + 首页 + + + 菜单 + + + 输入网址 + 页面缩放 diff --git a/SafeExamBrowser.UserInterface.Contracts/Browser/Data/JavascriptResult.cs b/SafeExamBrowser.UserInterface.Contracts/Browser/Data/JavascriptResult.cs new file mode 100644 index 00000000..82459ff0 --- /dev/null +++ b/SafeExamBrowser.UserInterface.Contracts/Browser/Data/JavascriptResult.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SafeExamBrowser.UserInterface.Contracts.Browser.Data +{ + /// + /// The data resulting from a Javascript expression evaluation. + /// + public class JavascriptResult + { + /// + /// Indicates if the Javascript was evaluated successfully or not. + /// + public bool Success { get; set; } + + /// + /// The error message, in case of an unsuccessful evaluation of the Javascript expression. + /// + public string Message { get; set; } + + /// + /// The data item returned by the Javascript expression. + /// + public dynamic Result { get; set; } + } +} diff --git a/SafeExamBrowser.UserInterface.Contracts/Browser/IBrowserControl.cs b/SafeExamBrowser.UserInterface.Contracts/Browser/IBrowserControl.cs index 3ec45476..a27c5c8e 100644 --- a/SafeExamBrowser.UserInterface.Contracts/Browser/IBrowserControl.cs +++ b/SafeExamBrowser.UserInterface.Contracts/Browser/IBrowserControl.cs @@ -6,6 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +using SafeExamBrowser.UserInterface.Contracts.Browser.Data; using SafeExamBrowser.UserInterface.Contracts.Browser.Events; namespace SafeExamBrowser.UserInterface.Contracts.Browser @@ -61,6 +62,11 @@ namespace SafeExamBrowser.UserInterface.Contracts.Browser /// void Destroy(); + /// + /// Executes the given Javascript code in the browser. + /// + void ExecuteJavascript(string javascript, System.Action callback); + /// /// Attempts to find the given term on the current page according to the specified parameters. /// diff --git a/SafeExamBrowser.UserInterface.Contracts/Browser/IBrowserWindow.cs b/SafeExamBrowser.UserInterface.Contracts/Browser/IBrowserWindow.cs index 970f2b02..b41cc25d 100644 --- a/SafeExamBrowser.UserInterface.Contracts/Browser/IBrowserWindow.cs +++ b/SafeExamBrowser.UserInterface.Contracts/Browser/IBrowserWindow.cs @@ -7,6 +7,7 @@ */ using System; +using SafeExamBrowser.Browser.Contracts.Events; using SafeExamBrowser.Core.Contracts.Resources.Icons; using SafeExamBrowser.UserInterface.Contracts.Browser.Data; using SafeExamBrowser.UserInterface.Contracts.Browser.Events; @@ -64,6 +65,11 @@ namespace SafeExamBrowser.UserInterface.Contracts.Browser /// event ActionRequestedEventHandler HomeNavigationRequested; + /// + /// Event fired when the browser window wants to lose focus to the taskbar. + /// + event LoseFocusRequestedEventHandler LoseFocusRequested; + /// /// Event fired when the user would like to reload the current page. /// @@ -123,5 +129,18 @@ namespace SafeExamBrowser.UserInterface.Contracts.Browser /// Updates the display value of the current page zoom. Value is expected to be in percentage. /// void UpdateZoomLevel(double value); + + /// + /// Sets the focus to the toolbar. + /// + /// If true, the first focusable control on the toolbar gets focused. If false, the last one. + void FocusToolbar(bool forward); + + /// + /// Sets the focus to the browser. + /// + void FocusBrowser(); + + void FocusAddressBar(); } } diff --git a/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs b/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs index 355c123c..d5447730 100644 --- a/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs +++ b/SafeExamBrowser.UserInterface.Contracts/IUserInterfaceFactory.cs @@ -56,7 +56,7 @@ namespace SafeExamBrowser.UserInterface.Contracts /// /// Creates a new browser window loaded with the given browser control and settings. /// - IBrowserWindow CreateBrowserWindow(IBrowserControl control, BrowserSettings settings, bool isMainWindow); + IBrowserWindow CreateBrowserWindow(IBrowserControl control, BrowserSettings settings, bool isMainWindow, ILogger logger); /// /// Creates an exam selection dialog for the given exams. diff --git a/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj b/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj index 21563d27..5eb5ab83 100644 --- a/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj +++ b/SafeExamBrowser.UserInterface.Contracts/SafeExamBrowser.UserInterface.Contracts.csproj @@ -55,6 +55,7 @@ + @@ -110,6 +111,10 @@ {ac77745d-3b41-43e2-8e84-d40e5a4ee77f} SafeExamBrowser.Applications.Contracts + + {5fb5273d-277c-41dd-8593-a25ce1aff2e9} + SafeExamBrowser.Browser.Contracts + {7d74555e-63e1-4c46-bd0a-8580552368c8} SafeExamBrowser.Configuration.Contracts diff --git a/SafeExamBrowser.UserInterface.Contracts/Shell/ITaskbar.cs b/SafeExamBrowser.UserInterface.Contracts/Shell/ITaskbar.cs index 76721115..0dd67e8a 100644 --- a/SafeExamBrowser.UserInterface.Contracts/Shell/ITaskbar.cs +++ b/SafeExamBrowser.UserInterface.Contracts/Shell/ITaskbar.cs @@ -6,6 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +using SafeExamBrowser.Browser.Contracts.Events; using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.UserInterface.Contracts.Shell.Events; @@ -31,6 +32,11 @@ namespace SafeExamBrowser.UserInterface.Contracts.Shell /// event QuitButtonClickedEventHandler QuitButtonClicked; + /// + /// Event fired when the Taskbar wants to lose focus. + /// + event LoseFocusRequestedEventHandler LoseFocusRequested; + /// /// Adds the given application control to the taskbar. /// @@ -70,5 +76,10 @@ namespace SafeExamBrowser.UserInterface.Contracts.Shell /// Shows the taskbar. /// void Show(); + + /// + /// Puts the focus on the taskbar. + /// + void Focus(bool forward = true); } } diff --git a/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/KeyboardLayoutButton.xaml b/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/KeyboardLayoutButton.xaml index abdcaff9..0e1f4445 100644 --- a/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/KeyboardLayoutButton.xaml +++ b/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/KeyboardLayoutButton.xaml @@ -5,7 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:fa="http://schemas.fontawesome.io/icons/" xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Desktop.Controls" - mc:Ignorable="d" d:DesignHeight="40" d:DesignWidth="250"> + mc:Ignorable="d" d:DesignHeight="40" d:DesignWidth="250" IsTabStop="True"> diff --git a/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/KeyboardLayoutControl.xaml b/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/KeyboardLayoutControl.xaml index 4ccf82d2..d991f4c1 100644 --- a/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/KeyboardLayoutControl.xaml +++ b/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/KeyboardLayoutControl.xaml @@ -16,7 +16,7 @@ - + diff --git a/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/KeyboardLayoutControl.xaml.cs b/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/KeyboardLayoutControl.xaml.cs index 5a1a09f9..24eaedf8 100644 --- a/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/KeyboardLayoutControl.xaml.cs +++ b/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenter/KeyboardLayoutControl.xaml.cs @@ -41,7 +41,14 @@ namespace SafeExamBrowser.UserInterface.Desktop.Controls.ActionCenter InitializeLayouts(); keyboard.LayoutChanged += Keyboard_LayoutChanged; - Button.Click += (o, args) => Popup.IsOpen = !Popup.IsOpen; + Button.Click += (o, args) => + { + Popup.IsOpen = !Popup.IsOpen; + this.Dispatcher.BeginInvoke((System.Action)(() => + { + LayoutsStackPanel.Children[0].Focus(); + })); + }; Button.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => Popup.IsOpen = Popup.IsMouseOver)); Popup.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => Popup.IsOpen = IsMouseOver)); Popup.Opened += (o, args) => Grid.Background = Brushes.Gray; @@ -90,5 +97,14 @@ namespace SafeExamBrowser.UserInterface.Desktop.Controls.ActionCenter Text.Text = layout.CultureName; Button.ToolTip = tooltip; } + + private void Popup_KeyUp(object sender, System.Windows.Input.KeyEventArgs e) + { + if (e.Key == System.Windows.Input.Key.Enter || e.Key == System.Windows.Input.Key.Escape) + { + Popup.IsOpen = false; + Button.Focus(); + } + } } } diff --git a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/AudioControl.xaml b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/AudioControl.xaml index cb433bf0..229251fc 100644 --- a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/AudioControl.xaml +++ b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/AudioControl.xaml @@ -16,7 +16,7 @@ - + @@ -24,7 +24,7 @@ - @@ -33,7 +33,7 @@ diff --git a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/AudioControl.xaml.cs b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/AudioControl.xaml.cs index 25427637..541416ad 100644 --- a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/AudioControl.xaml.cs +++ b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/AudioControl.xaml.cs @@ -60,6 +60,7 @@ namespace SafeExamBrowser.UserInterface.Desktop.Controls.Taskbar { Background = Brushes.LightGray; Button.Background = Brushes.LightGray; + Volume.Focus(); }; Popup.Closed += (o, args) => @@ -167,5 +168,23 @@ namespace SafeExamBrowser.UserInterface.Desktop.Controls.Taskbar return IconResourceLoader.Load(resource); } + + private void Popup_KeyDown(object sender, System.Windows.Input.KeyEventArgs e) + { + if (e.Key == System.Windows.Input.Key.Escape) + { + Popup.IsOpen = false; + Button.Focus(); + } + } + + private void Volume_KeyDown(object sender, System.Windows.Input.KeyEventArgs e) + { + if (e.Key == System.Windows.Input.Key.Enter) + { + Popup.IsOpen = false; + Button.Focus(); + } + } } } diff --git a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/Clock.xaml b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/Clock.xaml index 4d3ec857..93517d0f 100644 --- a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/Clock.xaml +++ b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/Clock.xaml @@ -11,7 +11,7 @@ - - + + diff --git a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/KeyboardLayoutButton.xaml b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/KeyboardLayoutButton.xaml index 90170748..20a0a91d 100644 --- a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/KeyboardLayoutButton.xaml +++ b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/KeyboardLayoutButton.xaml @@ -14,24 +14,22 @@ - - - + + + diff --git a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/KeyboardLayoutButton.xaml.cs b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/KeyboardLayoutButton.xaml.cs index 8dc22819..ad74c2fa 100644 --- a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/KeyboardLayoutButton.xaml.cs +++ b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/KeyboardLayoutButton.xaml.cs @@ -43,6 +43,7 @@ namespace SafeExamBrowser.UserInterface.Desktop.Controls.Taskbar CultureCodeTextBlock.Text = layout.CultureCode; CultureNameTextBlock.Text = layout.CultureName; LayoutNameTextBlock.Text = layout.LayoutName; + System.Windows.Automation.AutomationProperties.SetHelpText(Button, layout.LayoutName); } } } diff --git a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/KeyboardLayoutControl.xaml b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/KeyboardLayoutControl.xaml index 4f745fd4..ca2bcdf8 100644 --- a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/KeyboardLayoutControl.xaml +++ b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/KeyboardLayoutControl.xaml @@ -16,7 +16,7 @@ - + diff --git a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/KeyboardLayoutControl.xaml.cs b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/KeyboardLayoutControl.xaml.cs index a607a677..da280d12 100644 --- a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/KeyboardLayoutControl.xaml.cs +++ b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/KeyboardLayoutControl.xaml.cs @@ -45,7 +45,14 @@ namespace SafeExamBrowser.UserInterface.Desktop.Controls.Taskbar InitializeLayouts(); keyboard.LayoutChanged += Keyboard_LayoutChanged; - Button.Click += (o, args) => Popup.IsOpen = !Popup.IsOpen; + Button.Click += (o, args) => + { + Popup.IsOpen = !Popup.IsOpen; + Task.Delay(200).ContinueWith(_ => this.Dispatcher.BeginInvoke((System.Action)(() => + { + ((LayoutsStackPanel.Children[0] as ContentControl).Content as UIElement).Focus(); + }))); + }; Button.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => Popup.IsOpen = Popup.IsMouseOver)); Popup.CustomPopupPlacementCallback = new CustomPopupPlacementCallback(Popup_PlacementCallback); Popup.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => Popup.IsOpen = IsMouseOver)); @@ -114,5 +121,14 @@ namespace SafeExamBrowser.UserInterface.Desktop.Controls.Taskbar LayoutCultureCode.Text = layout.CultureCode; Button.ToolTip = tooltip; } + + private void Popup_KeyUp(object sender, System.Windows.Input.KeyEventArgs e) + { + if (e.Key == System.Windows.Input.Key.Enter || e.Key == System.Windows.Input.Key.Escape) + { + Popup.IsOpen = false; + Button.Focus(); + } + } } } diff --git a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/PowerSupplyControl.xaml b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/PowerSupplyControl.xaml index 09c2e8a1..7522d2e6 100644 --- a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/PowerSupplyControl.xaml +++ b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/PowerSupplyControl.xaml @@ -4,7 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Desktop.Controls" - mc:Ignorable="d" d:DesignHeight="40" d:DesignWidth="40"> + mc:Ignorable="d" d:DesignHeight="40" d:DesignWidth="40" Focusable="true" IsTabStop="True"> diff --git a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/PowerSupplyControl.xaml.cs b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/PowerSupplyControl.xaml.cs index 7b593b59..227a287d 100644 --- a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/PowerSupplyControl.xaml.cs +++ b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/PowerSupplyControl.xaml.cs @@ -8,6 +8,7 @@ using System; using System.Windows; +using System.Windows.Automation; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Media; @@ -101,6 +102,7 @@ namespace SafeExamBrowser.UserInterface.Desktop.Controls.Taskbar Button.ToolTip = tooltip; PowerPlug.Visibility = status.IsOnline ? Visibility.Visible : Visibility.Collapsed; Warning.Visibility = status.BatteryChargeStatus == BatteryChargeStatus.Critical ? Visibility.Visible : Visibility.Collapsed; + AutomationProperties.SetHelpText(this, Button.ToolTip as string); } private void RenderCharge(double charge, BatteryChargeStatus status) diff --git a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/QuitButton.xaml b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/QuitButton.xaml index 27a637d7..592ac0b4 100644 --- a/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/QuitButton.xaml +++ b/SafeExamBrowser.UserInterface.Desktop/Controls/Taskbar/QuitButton.xaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Desktop.Controls" + x:Name="root" mc:Ignorable="d" d:DesignHeight="40" d:DesignWidth="40"> @@ -15,6 +16,7 @@ - - @@ -80,7 +80,7 @@ - @@ -90,7 +90,7 @@ - diff --git a/SafeExamBrowser.UserInterface.Desktop/Windows/BrowserWindow.xaml.cs b/SafeExamBrowser.UserInterface.Desktop/Windows/BrowserWindow.xaml.cs index 99f2c2fe..2d4f4126 100644 --- a/SafeExamBrowser.UserInterface.Desktop/Windows/BrowserWindow.xaml.cs +++ b/SafeExamBrowser.UserInterface.Desktop/Windows/BrowserWindow.xaml.cs @@ -8,14 +8,17 @@ using System; using System.ComponentModel; +using System.Reflection; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Interop; using System.Windows.Media.Imaging; +using SafeExamBrowser.Browser.Contracts.Events; using SafeExamBrowser.Core.Contracts.Resources.Icons; using SafeExamBrowser.I18n.Contracts; +using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Settings.Browser; using SafeExamBrowser.UserInterface.Contracts; using SafeExamBrowser.UserInterface.Contracts.Browser; @@ -33,9 +36,13 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows private readonly bool isMainWindow; private readonly BrowserSettings settings; private readonly IText text; + private readonly ILogger logger; + private readonly IBrowserControl browserControl; private WindowClosedEventHandler closed; private WindowClosingEventHandler closing; + private bool browserControlGetsFocusFromTaskbar = false; + private IInputElement tabKeyDownFocusElement = null; private WindowSettings WindowSettings { @@ -56,6 +63,7 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows public event ActionRequestedEventHandler ZoomInRequested; public event ActionRequestedEventHandler ZoomOutRequested; public event ActionRequestedEventHandler ZoomResetRequested; + public event LoseFocusRequestedEventHandler LoseFocusRequested; event WindowClosedEventHandler IWindow.Closed { @@ -69,11 +77,13 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows remove { closing -= value; } } - internal BrowserWindow(IBrowserControl browserControl, BrowserSettings settings, bool isMainWindow, IText text) + internal BrowserWindow(IBrowserControl browserControl, BrowserSettings settings, bool isMainWindow, IText text, ILogger logger) { this.isMainWindow = isMainWindow; this.settings = settings; this.text = text; + this.logger = logger; + this.browserControl = browserControl; InitializeComponent(); InitializeBrowserWindow(browserControl); @@ -199,6 +209,33 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows } } + private void BrowserWindow_KeyDown(object sender, KeyEventArgs e) + { + 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) + { + var control = firstActiveElementInToolbar as System.Windows.UIElement; + if (control.IsKeyboardFocusWithin) + { + this.LoseFocusRequested?.Invoke(false); + e.Handled = true; + } + } + } + + tabKeyDownFocusElement = FocusManager.GetFocusedElement(this); + } + else + { + tabKeyDownFocusElement = null; + } + } + private void BrowserWindow_KeyUp(object sender, KeyEventArgs e) { if (e.Key == Key.F5) @@ -215,6 +252,65 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows { ShowFindbar(); } + + if (e.Key == Key.Tab) + { + 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) + { + Toolbar.Focus(); + } + else if (Toolbar.Visibility == Visibility.Hidden) + { + Findbar.Focus(); + } + } + else if (MenuPopup.IsKeyboardFocusWithin) + { + 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); + //var nextTab = GetNextTab(MenuPopup, this, true); + if (!hasShift && focusedControl.TabIndex < prevFocusedControl.TabIndex) + { + MenuPopup.IsOpen = false; + FocusBrowser(); + } + else if (hasShift && focusedControl.TabIndex > prevFocusedControl.TabIndex) + { + MenuPopup.IsOpen = false; + MenuButton.Focus(); + } + } + } + } + } + + /// + /// Get next tab order element. Copied from https://stackoverflow.com/questions/5756448/in-wpf-how-can-i-get-the-next-control-in-the-tab-order + /// + /// The element to get next tab order + /// The container element owning 'e'. Make sure this is a container of 'e'. + /// True if search only itself and inside of 'container'; otherwise false. + /// If true and next tab order element is outside of 'container', result in null. + /// Next tab order element or null if not found + public DependencyObject GetNextTab(DependencyObject e, DependencyObject container, bool goDownOnly) + { + var navigation = typeof(FrameworkElement) + .GetProperty("KeyboardNavigation", BindingFlags.NonPublic | BindingFlags.Static) + .GetValue(null); + + var method = navigation + .GetType() + .GetMethod("GetNextTab", BindingFlags.NonPublic | BindingFlags.Instance); + + return method.Invoke(navigation, new object[] { e, container, goDownOnly }) as DependencyObject; } private void BrowserWindow_Loaded(object sender, RoutedEventArgs e) @@ -320,10 +416,11 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows ForwardButton.Click += (o, args) => ForwardNavigationRequested?.Invoke(); HomeButton.Click += (o, args) => HomeNavigationRequested?.Invoke(); Loaded += BrowserWindow_Loaded; - MenuButton.Click += (o, args) => MenuPopup.IsOpen = !MenuPopup.IsOpen; + MenuButton.Click += MenuButton_Click; MenuButton.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => MenuPopup.IsOpen = MenuPopup.IsMouseOver)); MenuPopup.CustomPopupPlacementCallback = new CustomPopupPlacementCallback(Popup_PlacementCallback); MenuPopup.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => MenuPopup.IsOpen = MenuPopup.IsMouseOver)); + KeyDown += BrowserWindow_KeyDown; KeyUp += BrowserWindow_KeyUp; LocationChanged += (o, args) => { DownloadsPopup.IsOpen = false; MenuPopup.IsOpen = false; }; ReloadButton.Click += (o, args) => ReloadRequested?.Invoke(); @@ -338,6 +435,48 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows ZoomInButton.Click += (o, args) => ZoomInRequested?.Invoke(); ZoomOutButton.Click += (o, args) => ZoomOutRequested?.Invoke(); ZoomResetButton.Click += (o, args) => ZoomResetRequested?.Invoke(); + BrowserControlHost.GotKeyboardFocus += BrowserControlHost_GotKeyboardFocus; + } + + private void MenuButton_Click(object sender, RoutedEventArgs e) + { + MenuPopup.IsOpen = !MenuPopup.IsOpen; + ZoomInButton.Focus(); + } + + private void BrowserControlHost_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) + { + var forward = !this.browserControlGetsFocusFromTaskbar; + + // focus the first / last element on the page + 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); + } +}"; + this.browserControl.ExecuteJavascript(javascript, result => + { + if (!result.Success) + { + logger.Error($"Javascript error {result.Message}!"); + } + }); + + this.browserControl.ExecuteJavascript("__SEB_focusElement(" + forward.ToString().ToLower() + ")", result => + { + if (!result.Success) + { + logger.Error($"Javascript error {result.Message}!"); + } + }); } private void ApplySettings() @@ -443,6 +582,58 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows FindCaseSensitiveCheckBox.Content = text.Get(TextKey.BrowserWindow_FindCaseSensitive); FindMenuText.Text = text.Get(TextKey.BrowserWindow_FindMenuItem); ZoomText.Text = text.Get(TextKey.BrowserWindow_ZoomMenuItem); + ReloadButton.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, text.Get(TextKey.BrowserWindow_ReloadButton)); + BackwardButton.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, text.Get(TextKey.BrowserWindow_BackwardButton)); + ForwardButton.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, text.Get(TextKey.BrowserWindow_ForwardButton)); + DownloadsButton.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, text.Get(TextKey.BrowserWindow_DownloadsButton)); + HomeButton.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, text.Get(TextKey.BrowserWindow_HomeButton)); + MenuButton.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, text.Get(TextKey.BrowserWindow_MenuButton)); + UrlTextBox.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, text.Get(TextKey.BrowserWindow_UrlTextBox)); + } + + public void FocusToolbar(bool forward) + { + this.Dispatcher.BeginInvoke((Action)(async () => + { + this.Activate(); + await Task.Delay(50); + + // focus all elements in the toolbar, such that the last element that is enabled gets focus + var buttons = new System.Windows.Controls.Control[] { ForwardButton, BackwardButton, ReloadButton, UrlTextBox, MenuButton, }; + for (var i = forward ? 0 : buttons.Length - 1; i >= 0 && i < buttons.Length; i += forward ? 1 : -1) + { + if (buttons[i].IsEnabled && buttons[i].Visibility == Visibility.Visible) + { + buttons[i].Focus(); + break; + } + } + })); + } + + public void FocusBrowser() + { + this.Dispatcher.BeginInvoke((Action)(async () => + { + this.FocusToolbar(false); + await Task.Delay(100); + + this.browserControlGetsFocusFromTaskbar = true; + + var focusedElement = FocusManager.GetFocusedElement(this) as UIElement; + focusedElement.MoveFocus(new TraversalRequest(FocusNavigationDirection.Right)); + + await Task.Delay(150); + this.browserControlGetsFocusFromTaskbar = false; + })); + } + + public void FocusAddressBar() + { + this.Dispatcher.BeginInvoke((Action)(async () => + { + this.UrlTextBox.Focus(); + })); } } } diff --git a/SafeExamBrowser.UserInterface.Desktop/Windows/Taskbar.xaml b/SafeExamBrowser.UserInterface.Desktop/Windows/Taskbar.xaml index 80199e63..2089f0d7 100644 --- a/SafeExamBrowser.UserInterface.Desktop/Windows/Taskbar.xaml +++ b/SafeExamBrowser.UserInterface.Desktop/Windows/Taskbar.xaml @@ -6,6 +6,7 @@ xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Desktop.Controls.Taskbar" xmlns:s="clr-namespace:System;assembly=mscorlib" mc:Ignorable="d" Title="Taskbar" Background="{DynamicResource BackgroundBrush}" Height="40" Width="750" WindowStyle="None" Topmost="True" + KeyDown="Window_KeyDown" KeyUp="Window_KeyUp" ResizeMode="NoResize" Icon="../Images/SafeExamBrowser.ico"> diff --git a/SafeExamBrowser.UserInterface.Desktop/Windows/Taskbar.xaml.cs b/SafeExamBrowser.UserInterface.Desktop/Windows/Taskbar.xaml.cs index eca46944..f4a184d2 100644 --- a/SafeExamBrowser.UserInterface.Desktop/Windows/Taskbar.xaml.cs +++ b/SafeExamBrowser.UserInterface.Desktop/Windows/Taskbar.xaml.cs @@ -6,8 +6,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +using System; using System.ComponentModel; using System.Windows; +using SafeExamBrowser.Browser.Contracts.Events; using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.UserInterface.Contracts.Shell; @@ -20,6 +22,8 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows { private bool allowClose; private ILogger logger; + private bool isQuitButtonFocusedAtKeyDown; + private bool isFirstChildFocusedAtKeyDown; public bool ShowClock { @@ -31,6 +35,7 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows set { Dispatcher.Invoke(() => QuitButton.Visibility = value ? Visibility.Visible : Visibility.Collapsed); } } + public event LoseFocusRequestedEventHandler LoseFocusRequested; public event QuitButtonClickedEventHandler QuitButtonClicked; internal Taskbar(ILogger logger) @@ -120,7 +125,9 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows { Dispatcher.Invoke(() => { - QuitButton.ToolTip = text.Get(TextKey.Shell_QuitButton); + var txt = text.Get(TextKey.Shell_QuitButton); + QuitButton.ToolTip = txt; + QuitButton.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, txt); }); } @@ -159,5 +166,82 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows 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; + } + + private void Window_KeyUp(object sender, System.Windows.Input.KeyEventArgs e) + { + 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) + { + this.LoseFocusRequested?.Invoke(true); + e.Handled = true; + } + else if (shift && this.QuitButton.IsKeyboardFocusWithin && this.isFirstChildFocusedAtKeyDown) + { + this.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(); + } + })); + } + + private bool SetFocusWithin(UIElement uIElement) + { + 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])) + { + return true; + } + } + + return false; + } + else if (uIElement is System.Windows.Controls.ContentControl) + { + var control = uIElement as System.Windows.Controls.ContentControl; + var content = control.Content as UIElement; + if (content != null) + { + return this.SetFocusWithin(content); + } + } + + return false; + } } } diff --git a/SafeExamBrowser.UserInterface.Mobile/SafeExamBrowser.UserInterface.Mobile.csproj b/SafeExamBrowser.UserInterface.Mobile/SafeExamBrowser.UserInterface.Mobile.csproj index 2a43e1aa..104239ef 100644 --- a/SafeExamBrowser.UserInterface.Mobile/SafeExamBrowser.UserInterface.Mobile.csproj +++ b/SafeExamBrowser.UserInterface.Mobile/SafeExamBrowser.UserInterface.Mobile.csproj @@ -207,6 +207,10 @@ {ac77745d-3b41-43e2-8e84-d40e5a4ee77f} SafeExamBrowser.Applications.Contracts + + {5fb5273d-277c-41dd-8593-a25ce1aff2e9} + SafeExamBrowser.Browser.Contracts + {7d74555e-63e1-4c46-bd0a-8580552368c8} SafeExamBrowser.Configuration.Contracts diff --git a/SafeExamBrowser.UserInterface.Mobile/UserInterfaceFactory.cs b/SafeExamBrowser.UserInterface.Mobile/UserInterfaceFactory.cs index 3625a500..35b851af 100644 --- a/SafeExamBrowser.UserInterface.Mobile/UserInterfaceFactory.cs +++ b/SafeExamBrowser.UserInterface.Mobile/UserInterfaceFactory.cs @@ -80,9 +80,9 @@ namespace SafeExamBrowser.UserInterface.Mobile } } - public IBrowserWindow CreateBrowserWindow(IBrowserControl control, BrowserSettings settings, bool isMainWindow) + public IBrowserWindow CreateBrowserWindow(IBrowserControl control, BrowserSettings settings, bool isMainWindow, ILogger logger) { - return Application.Current.Dispatcher.Invoke(() => new BrowserWindow(control, settings, isMainWindow, text)); + return Application.Current.Dispatcher.Invoke(() => new BrowserWindow(control, settings, isMainWindow, text, logger)); } public IExamSelectionDialog CreateExamSelectionDialog(IEnumerable exams) diff --git a/SafeExamBrowser.UserInterface.Mobile/Windows/BrowserWindow.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Windows/BrowserWindow.xaml.cs index f2ffe1b4..8110d4d9 100644 --- a/SafeExamBrowser.UserInterface.Mobile/Windows/BrowserWindow.xaml.cs +++ b/SafeExamBrowser.UserInterface.Mobile/Windows/BrowserWindow.xaml.cs @@ -14,8 +14,10 @@ using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Interop; using System.Windows.Media.Imaging; +using SafeExamBrowser.Browser.Contracts.Events; using SafeExamBrowser.Core.Contracts.Resources.Icons; using SafeExamBrowser.I18n.Contracts; +using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Settings.Browser; using SafeExamBrowser.UserInterface.Contracts; using SafeExamBrowser.UserInterface.Contracts.Browser; @@ -36,6 +38,7 @@ namespace SafeExamBrowser.UserInterface.Mobile.Windows private WindowClosedEventHandler closed; private WindowClosingEventHandler closing; + private ILogger logger; private WindowSettings WindowSettings { @@ -51,6 +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 ActionRequestedEventHandler HomeNavigationRequested; public event ActionRequestedEventHandler ReloadRequested; public event ActionRequestedEventHandler ZoomInRequested; @@ -69,11 +73,12 @@ namespace SafeExamBrowser.UserInterface.Mobile.Windows remove { closing -= value; } } - internal BrowserWindow(IBrowserControl browserControl, BrowserSettings settings, bool isMainWindow, IText text) + internal BrowserWindow(IBrowserControl browserControl, BrowserSettings settings, bool isMainWindow, IText text, ILogger logger) { this.isMainWindow = isMainWindow; this.settings = settings; this.text = text; + this.logger = logger; InitializeComponent(); InitializeBrowserWindow(browserControl); @@ -454,5 +459,25 @@ namespace SafeExamBrowser.UserInterface.Mobile.Windows FindMenuText.Text = text.Get(TextKey.BrowserWindow_FindMenuItem); ZoomText.Text = text.Get(TextKey.BrowserWindow_ZoomMenuItem); } + + public void FocusToolbar(bool forward) + { + throw new NotImplementedException(); + } + + public void FocusBrowser() + { + throw new NotImplementedException(); + } + + public void Debug() + { + throw new NotImplementedException(); + } + + public void FocusAddressBar() + { + this.UrlTextBox.Focus(); + } } } diff --git a/SafeExamBrowser.UserInterface.Mobile/Windows/Taskbar.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Windows/Taskbar.xaml.cs index c47469d8..9ddfee07 100644 --- a/SafeExamBrowser.UserInterface.Mobile/Windows/Taskbar.xaml.cs +++ b/SafeExamBrowser.UserInterface.Mobile/Windows/Taskbar.xaml.cs @@ -8,6 +8,7 @@ using System.ComponentModel; using System.Windows; +using SafeExamBrowser.Browser.Contracts.Events; using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.UserInterface.Contracts.Shell; @@ -31,6 +32,7 @@ namespace SafeExamBrowser.UserInterface.Mobile.Windows set { Dispatcher.Invoke(() => QuitButton.Visibility = value ? Visibility.Visible : Visibility.Collapsed); } } + public event LoseFocusRequestedEventHandler LoseFocusRequested; public event QuitButtonClickedEventHandler QuitButtonClicked; internal Taskbar(ILogger logger) @@ -159,5 +161,18 @@ namespace SafeExamBrowser.UserInterface.Mobile.Windows 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(); + } + } } }