From 18b8f6630017f7dfb9a6284444def50e45a456fc Mon Sep 17 00:00:00 2001 From: dbuechel Date: Tue, 30 Jan 2018 14:41:36 +0100 Subject: [PATCH] SEBWIN-219: Continued implementing kiosk operation and realized that further changes to startup/runtime/shutdown-mechanism are necessary... --- .../Settings/Settings.cs | 1 + .../Behaviour/IRuntimeController.cs | 5 ++ .../Configuration/Settings/ISettings.cs | 7 +- .../Configuration/Settings/KioskMode.cs | 28 ++++++ SafeExamBrowser.Contracts/I18n/TextKey.cs | 4 + .../SafeExamBrowser.Contracts.csproj | 1 + .../UserInterface/IRuntimeWindow.cs | 5 +- .../UserInterface/ISplashScreen.cs | 12 +-- .../UserInterface/IUserInterfaceFactory.cs | 6 ++ .../UserInterface/IWindow.cs | 5 ++ .../Behaviour/ShutdownController.cs | 4 +- .../Behaviour/StartupController.cs | 4 +- SafeExamBrowser.Core/I18n/Text.xml | 12 +++ SafeExamBrowser.Runtime/App.cs | 14 +-- .../Operations/KioskModeOperation.cs | 60 ++++++++++++- .../Behaviour/RuntimeController.cs | 87 ++++++++++++++----- SafeExamBrowser.Runtime/CompositionRoot.cs | 9 +- .../BrowserWindow.xaml.cs | 26 +++++- .../LogWindow.xaml.cs | 5 ++ .../RuntimeWindow.xaml | 8 +- .../RuntimeWindow.xaml.cs | 47 +++++++++- ...feExamBrowser.UserInterface.Classic.csproj | 2 + .../SplashScreen.xaml.cs | 32 ++++++- .../UserInterfaceFactory.cs | 25 ++++++ .../Utilities/RuntimeWindowLogFormatter.cs | 39 +++++++++ .../ViewModels/RuntimeWindowViewModel.cs | 70 +++++++++++++++ .../BrowserWindow.xaml.cs | 26 +++++- .../LogWindow.xaml.cs | 5 ++ .../SplashScreen.xaml.cs | 32 ++++++- .../UserInterfaceFactory.cs | 6 ++ 30 files changed, 505 insertions(+), 82 deletions(-) create mode 100644 SafeExamBrowser.Contracts/Configuration/Settings/KioskMode.cs create mode 100644 SafeExamBrowser.UserInterface.Classic/Utilities/RuntimeWindowLogFormatter.cs create mode 100644 SafeExamBrowser.UserInterface.Classic/ViewModels/RuntimeWindowViewModel.cs diff --git a/SafeExamBrowser.Configuration/Settings/Settings.cs b/SafeExamBrowser.Configuration/Settings/Settings.cs index f7b8b67a..25159977 100644 --- a/SafeExamBrowser.Configuration/Settings/Settings.cs +++ b/SafeExamBrowser.Configuration/Settings/Settings.cs @@ -15,6 +15,7 @@ namespace SafeExamBrowser.Configuration.Settings internal class Settings : ISettings { public ConfigurationMode ConfigurationMode { get; set; } + public KioskMode KioskMode { get; set; } public ServicePolicy ServicePolicy { get; set; } public IBrowserSettings Browser { get; set; } diff --git a/SafeExamBrowser.Contracts/Behaviour/IRuntimeController.cs b/SafeExamBrowser.Contracts/Behaviour/IRuntimeController.cs index 675b1776..4b8e7c3e 100644 --- a/SafeExamBrowser.Contracts/Behaviour/IRuntimeController.cs +++ b/SafeExamBrowser.Contracts/Behaviour/IRuntimeController.cs @@ -14,5 +14,10 @@ namespace SafeExamBrowser.Contracts.Behaviour /// Reverts any changes performed during the startup or runtime and releases all used resources. /// void FinalizeApplication(); + + /// + /// Initializes a new session and starts performing the runtime logic / event handling. + /// + void StartSession(); } } diff --git a/SafeExamBrowser.Contracts/Configuration/Settings/ISettings.cs b/SafeExamBrowser.Contracts/Configuration/Settings/ISettings.cs index 21dbda4e..81aa6b93 100644 --- a/SafeExamBrowser.Contracts/Configuration/Settings/ISettings.cs +++ b/SafeExamBrowser.Contracts/Configuration/Settings/ISettings.cs @@ -25,13 +25,18 @@ namespace SafeExamBrowser.Contracts.Configuration.Settings /// IKeyboardSettings Keyboard { get; } + /// + /// The kiosk mode which determines how the computer is locked down. + /// + KioskMode KioskMode { get; } + /// /// All mouse-related settings. /// IMouseSettings Mouse { get; } /// - /// The active service policy. + /// The active policy for the service component. /// ServicePolicy ServicePolicy { get; } diff --git a/SafeExamBrowser.Contracts/Configuration/Settings/KioskMode.cs b/SafeExamBrowser.Contracts/Configuration/Settings/KioskMode.cs new file mode 100644 index 00000000..5c3da31c --- /dev/null +++ b/SafeExamBrowser.Contracts/Configuration/Settings/KioskMode.cs @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2018 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.Contracts.Configuration.Settings +{ + public enum KioskMode + { + /// + /// No kiosk mode - should only be used for testing / debugging. + /// + None, + + /// + /// Creates a new desktop and runs the client application on it, without modifying the default desktop. + /// + CreateNewDesktop, + + /// + /// Terminates the Windows explorer shell and runs the client application on the default desktop. + /// + DisableExplorerShell + } +} diff --git a/SafeExamBrowser.Contracts/I18n/TextKey.cs b/SafeExamBrowser.Contracts/I18n/TextKey.cs index 93fbfb11..8546c53f 100644 --- a/SafeExamBrowser.Contracts/I18n/TextKey.cs +++ b/SafeExamBrowser.Contracts/I18n/TextKey.cs @@ -26,16 +26,20 @@ namespace SafeExamBrowser.Contracts.I18n Notification_AboutTooltip, Notification_LogTooltip, RuntimeWindow_ApplicationRunning, + RuntimeWindow_StartSession, + RuntimeWindow_StopSession, SplashScreen_CloseServiceConnection, SplashScreen_EmptyClipboard, SplashScreen_InitializeBrowser, SplashScreen_InitializeConfiguration, + SplashScreen_InitializeKioskMode, SplashScreen_InitializeProcessMonitoring, SplashScreen_InitializeServiceConnection, SplashScreen_InitializeTaskbar, SplashScreen_InitializeWindowMonitoring, SplashScreen_InitializeWorkingArea, SplashScreen_RestoreWorkingArea, + SplashScreen_RevertKioskMode, SplashScreen_ShutdownProcedure, SplashScreen_StartEventHandling, SplashScreen_StartKeyboardInterception, diff --git a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj index f0e3a0e0..612cea5a 100644 --- a/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj +++ b/SafeExamBrowser.Contracts/SafeExamBrowser.Contracts.csproj @@ -81,6 +81,7 @@ + diff --git a/SafeExamBrowser.Contracts/UserInterface/IRuntimeWindow.cs b/SafeExamBrowser.Contracts/UserInterface/IRuntimeWindow.cs index bfb6ee9b..eae421e4 100644 --- a/SafeExamBrowser.Contracts/UserInterface/IRuntimeWindow.cs +++ b/SafeExamBrowser.Contracts/UserInterface/IRuntimeWindow.cs @@ -7,15 +7,16 @@ */ using SafeExamBrowser.Contracts.I18n; +using SafeExamBrowser.Contracts.Logging; namespace SafeExamBrowser.Contracts.UserInterface { - public interface IRuntimeWindow : IWindow + public interface IRuntimeWindow : ILogObserver, IWindow { /// /// Updates the status text of the runtime window. If the busy flag is set, /// the window will show an animation to indicate a long-running operation. /// - void UpdateStatus(TextKey key); + void UpdateStatus(TextKey key, bool showBusyIndication = false); } } diff --git a/SafeExamBrowser.Contracts/UserInterface/ISplashScreen.cs b/SafeExamBrowser.Contracts/UserInterface/ISplashScreen.cs index 93a1a1c4..4b04e592 100644 --- a/SafeExamBrowser.Contracts/UserInterface/ISplashScreen.cs +++ b/SafeExamBrowser.Contracts/UserInterface/ISplashScreen.cs @@ -10,18 +10,8 @@ using SafeExamBrowser.Contracts.I18n; namespace SafeExamBrowser.Contracts.UserInterface { - public interface ISplashScreen + public interface ISplashScreen : IWindow { - /// - /// Closes the splash screen on its own thread. - /// - void InvokeClose(); - - /// - /// Shows the splash screen on its own thread. - /// - void InvokeShow(); - /// /// Updates the progress bar of the splash screen according to the specified amount. /// diff --git a/SafeExamBrowser.Contracts/UserInterface/IUserInterfaceFactory.cs b/SafeExamBrowser.Contracts/UserInterface/IUserInterfaceFactory.cs index 33ab4511..c5ce81f3 100644 --- a/SafeExamBrowser.Contracts/UserInterface/IUserInterfaceFactory.cs +++ b/SafeExamBrowser.Contracts/UserInterface/IUserInterfaceFactory.cs @@ -51,6 +51,12 @@ namespace SafeExamBrowser.Contracts.UserInterface /// ISystemPowerSupplyControl CreatePowerSupplyControl(); + /// + /// Creates a new runtime window which runs on its own thread. + /// + /// + IRuntimeWindow CreateRuntimeWindow(IRuntimeInfo runtimeInfo, IText text); + /// /// Creates a new splash screen which runs on its own thread. /// diff --git a/SafeExamBrowser.Contracts/UserInterface/IWindow.cs b/SafeExamBrowser.Contracts/UserInterface/IWindow.cs index 69b37802..813e262d 100644 --- a/SafeExamBrowser.Contracts/UserInterface/IWindow.cs +++ b/SafeExamBrowser.Contracts/UserInterface/IWindow.cs @@ -27,6 +27,11 @@ namespace SafeExamBrowser.Contracts.UserInterface /// void Close(); + /// + /// Hides the window. + /// + void Hide(); + /// /// Shows the window to the user. /// diff --git a/SafeExamBrowser.Core/Behaviour/ShutdownController.cs b/SafeExamBrowser.Core/Behaviour/ShutdownController.cs index 91c6799b..0c38c73e 100644 --- a/SafeExamBrowser.Core/Behaviour/ShutdownController.cs +++ b/SafeExamBrowser.Core/Behaviour/ShutdownController.cs @@ -72,7 +72,7 @@ namespace SafeExamBrowser.Core.Behaviour splashScreen = uiFactory.CreateSplashScreen(runtimeInfo, text); splashScreen.SetIndeterminate(); splashScreen.UpdateText(TextKey.SplashScreen_ShutdownProcedure); - splashScreen.InvokeShow(); + splashScreen.Show(); } private void LogAndShowException(Exception e) @@ -92,7 +92,7 @@ namespace SafeExamBrowser.Core.Behaviour logger.Info("--- Shutdown procedure failed! ---"); } - splashScreen?.InvokeClose(); + splashScreen?.Close(); } } } diff --git a/SafeExamBrowser.Core/Behaviour/StartupController.cs b/SafeExamBrowser.Core/Behaviour/StartupController.cs index d4bad03f..881bbc14 100644 --- a/SafeExamBrowser.Core/Behaviour/StartupController.cs +++ b/SafeExamBrowser.Core/Behaviour/StartupController.cs @@ -117,7 +117,7 @@ namespace SafeExamBrowser.Core.Behaviour splashScreen = uiFactory.CreateSplashScreen(runtimeInfo, text); splashScreen.SetMaxProgress(operationCount); splashScreen.UpdateText(TextKey.SplashScreen_StartupProcedure); - splashScreen.InvokeShow(); + splashScreen.Show(); } private void LogAndShowException(Exception e) @@ -139,7 +139,7 @@ namespace SafeExamBrowser.Core.Behaviour logger.Info("--- Startup procedure aborted! ---"); } - splashScreen?.InvokeClose(); + splashScreen?.Close(); } } } diff --git a/SafeExamBrowser.Core/I18n/Text.xml b/SafeExamBrowser.Core/I18n/Text.xml index 323e7e30..5755e73a 100644 --- a/SafeExamBrowser.Core/I18n/Text.xml +++ b/SafeExamBrowser.Core/I18n/Text.xml @@ -33,6 +33,12 @@ The application is running. + + Starting new session + + + Stopping current session + Closing service connection @@ -45,6 +51,9 @@ Initializing application configuration + + Initializing kiosk mode + Initializing process monitoring @@ -63,6 +72,9 @@ Restoring working area + + Reverting kiosk mode + Initiating shutdown procedure diff --git a/SafeExamBrowser.Runtime/App.cs b/SafeExamBrowser.Runtime/App.cs index 65e98b1f..292d35e8 100644 --- a/SafeExamBrowser.Runtime/App.cs +++ b/SafeExamBrowser.Runtime/App.cs @@ -7,7 +7,6 @@ */ using System; -using System.ComponentModel; using System.Threading; using System.Windows; @@ -56,6 +55,8 @@ namespace SafeExamBrowser.Runtime { base.OnStartup(e); + ShutdownMode = ShutdownMode.OnExplicitShutdown; + instances.BuildObjectGraph(); instances.LogStartupInformation(); @@ -63,9 +64,7 @@ namespace SafeExamBrowser.Runtime if (success) { - MainWindow = instances.RuntimeWindow; - MainWindow.Closing += MainWindow_Closing; - MainWindow.Show(); + instances.RuntimeController.StartSession(); } else { @@ -75,15 +74,10 @@ namespace SafeExamBrowser.Runtime protected override void OnExit(ExitEventArgs e) { + instances.RuntimeController.FinalizeApplication(); instances.LogShutdownInformation(); base.OnExit(e); } - - private void MainWindow_Closing(object sender, CancelEventArgs e) - { - MainWindow.Hide(); - instances.RuntimeController.FinalizeApplication(); - } } } diff --git a/SafeExamBrowser.Runtime/Behaviour/Operations/KioskModeOperation.cs b/SafeExamBrowser.Runtime/Behaviour/Operations/KioskModeOperation.cs index 9e0c917b..8014d41d 100644 --- a/SafeExamBrowser.Runtime/Behaviour/Operations/KioskModeOperation.cs +++ b/SafeExamBrowser.Runtime/Behaviour/Operations/KioskModeOperation.cs @@ -6,25 +6,79 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -using System; using SafeExamBrowser.Contracts.Behaviour; +using SafeExamBrowser.Contracts.Configuration.Settings; +using SafeExamBrowser.Contracts.I18n; +using SafeExamBrowser.Contracts.Logging; using SafeExamBrowser.Contracts.UserInterface; namespace SafeExamBrowser.Runtime.Behaviour.Operations { internal class KioskModeOperation : IOperation { + private ILogger logger; + private ISettingsRepository settingsRepository; + private KioskMode kioskMode; + public bool AbortStartup { get; private set; } public ISplashScreen SplashScreen { private get; set; } + public KioskModeOperation(ILogger logger, ISettingsRepository settingsRepository) + { + this.logger = logger; + this.settingsRepository = settingsRepository; + } + public void Perform() { - // TODO + kioskMode = settingsRepository.Current.KioskMode; + + logger.Info($"Initializing kiosk mode '{kioskMode}'..."); + SplashScreen.UpdateText(TextKey.SplashScreen_InitializeKioskMode); + + if (kioskMode == KioskMode.CreateNewDesktop) + { + CreateNewDesktop(); + } + else + { + DisableExplorerShell(); + } } public void Revert() { - // TODO + logger.Info($"Reverting kiosk mode '{kioskMode}'..."); + SplashScreen.UpdateText(TextKey.SplashScreen_RevertKioskMode); + + if (kioskMode == KioskMode.CreateNewDesktop) + { + CloseNewDesktop(); + } + else + { + RestartExplorerShell(); + } + } + + private void CreateNewDesktop() + { + + } + + private void CloseNewDesktop() + { + + } + + private void DisableExplorerShell() + { + + } + + private void RestartExplorerShell() + { + } } } diff --git a/SafeExamBrowser.Runtime/Behaviour/RuntimeController.cs b/SafeExamBrowser.Runtime/Behaviour/RuntimeController.cs index ba7841af..e5bfd7c2 100644 --- a/SafeExamBrowser.Runtime/Behaviour/RuntimeController.cs +++ b/SafeExamBrowser.Runtime/Behaviour/RuntimeController.cs @@ -6,10 +6,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +using System; using System.Collections.Generic; using System.Linq; using SafeExamBrowser.Contracts.Behaviour; using SafeExamBrowser.Contracts.Communication; +using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.Configuration.Settings; using SafeExamBrowser.Contracts.I18n; using SafeExamBrowser.Contracts.Logging; @@ -19,66 +21,107 @@ namespace SafeExamBrowser.Runtime.Behaviour { internal class RuntimeController : IRuntimeController { - private ICommunication serviceProxy; private Queue operations; private ILogger logger; + private IRuntimeInfo runtimeInfo; private IRuntimeWindow runtimeWindow; + private IServiceProxy serviceProxy; private ISettingsRepository settingsRepository; private IShutdownController shutdownController; private IStartupController startupController; - - public ISettings Settings { private get; set; } + private Action terminationCallback; + private IText text; + private IUserInterfaceFactory uiFactory; public RuntimeController( - ICommunication serviceProxy, ILogger logger, - IRuntimeWindow runtimeWindow, + IRuntimeInfo runtimeInfo, + IServiceProxy serviceProxy, ISettingsRepository settingsRepository, IShutdownController shutdownController, - IStartupController startupController) + IStartupController startupController, + Action terminationCallback, + IText text, + IUserInterfaceFactory uiFactory) { - this.serviceProxy = serviceProxy; this.logger = logger; - this.runtimeWindow = runtimeWindow; + this.runtimeInfo = runtimeInfo; + this.serviceProxy = serviceProxy; this.settingsRepository = settingsRepository; this.shutdownController = shutdownController; this.startupController = startupController; + this.terminationCallback = terminationCallback; + this.text = text; + this.uiFactory = uiFactory; operations = new Queue(); } public bool TryInitializeApplication(Queue operations) { - operations = new Queue(operations); - var success = startupController.TryInitializeApplication(operations); + runtimeWindow = uiFactory.CreateRuntimeWindow(runtimeInfo, text); + if (success) { - Start(); + this.operations = new Queue(operations); + logger.Subscribe(runtimeWindow); } return success; } + public void StartSession() + { + runtimeWindow.Show(); + + logger.Info("Starting new session..."); + runtimeWindow.UpdateStatus(TextKey.RuntimeWindow_StartSession, true); + + // TODO: + // - Initialize configuration + // - Initialize kiosk mode + // - Initialize session data + // - Start runtime communication host + // - Create and connect to client + // - Initialize session with service + // - Verify session integrity and start event handling + System.Threading.Thread.Sleep(10000); + + runtimeWindow.UpdateStatus(TextKey.RuntimeWindow_ApplicationRunning); + + if (settingsRepository.Current.KioskMode == KioskMode.DisableExplorerShell) + { + runtimeWindow.Hide(); + } + } + public void FinalizeApplication() { - Stop(); + StopSession(); + + // TODO: + // - Disconnect from service + // - Terminate runtime communication host + // - Revert kiosk mode (or do that when stopping session?) + + logger.Unsubscribe(runtimeWindow); + runtimeWindow.Close(); shutdownController.FinalizeApplication(new Queue(operations.Reverse())); } - private void Start() + private void StopSession() { - logger.Info("Starting event handling..."); - // TODO SplashScreen.UpdateText(TextKey.SplashScreen_StartEventHandling); + logger.Info("Stopping current session..."); + runtimeWindow.Show(); + runtimeWindow.BringToForeground(); + runtimeWindow.UpdateStatus(TextKey.RuntimeWindow_StopSession, true); - runtimeWindow.UpdateStatus(TextKey.RuntimeWindow_ApplicationRunning); - } - - private void Stop() - { - logger.Info("Stopping event handling..."); - // TODO SplashScreen.UpdateText(TextKey.SplashScreen_StopEventHandling); + // TODO: + // - Terminate client (or does it terminate itself?) + // - Finalize session with service + // - Stop event handling and close session } } } diff --git a/SafeExamBrowser.Runtime/CompositionRoot.cs b/SafeExamBrowser.Runtime/CompositionRoot.cs index 56e9285d..0c58fc06 100644 --- a/SafeExamBrowser.Runtime/CompositionRoot.cs +++ b/SafeExamBrowser.Runtime/CompositionRoot.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Reflection; +using System.Windows; using SafeExamBrowser.Configuration; using SafeExamBrowser.Configuration.Settings; using SafeExamBrowser.Contracts.Behaviour; @@ -35,7 +36,6 @@ namespace SafeExamBrowser.Runtime internal IRuntimeController RuntimeController { get; private set; } internal Queue StartupOperations { get; private set; } - internal RuntimeWindow RuntimeWindow { get; private set; } internal void BuildObjectGraph() { @@ -56,16 +56,13 @@ namespace SafeExamBrowser.Runtime var shutdownController = new ShutdownController(logger, runtimeInfo, text, uiFactory); var startupController = new StartupController(logger, runtimeInfo, systemInfo, text, uiFactory); - RuntimeWindow = new RuntimeWindow(new DefaultLogFormatter(), runtimeInfo, text); - RuntimeController = new RuntimeController(serviceProxy, new ModuleLogger(logger, typeof(RuntimeController)), RuntimeWindow, settingsRepository, shutdownController, startupController); - - logger.Subscribe(RuntimeWindow); + RuntimeController = new RuntimeController(new ModuleLogger(logger, typeof(RuntimeController)), runtimeInfo, serviceProxy, settingsRepository, shutdownController, startupController, Application.Current.Shutdown, text, uiFactory); StartupOperations = new Queue(); StartupOperations.Enqueue(new I18nOperation(logger, text)); StartupOperations.Enqueue(new ConfigurationOperation(logger, runtimeInfo, settingsRepository, text, uiFactory, args)); StartupOperations.Enqueue(new ServiceOperation(logger, serviceProxy, settingsRepository, text)); - StartupOperations.Enqueue(new KioskModeOperation()); + StartupOperations.Enqueue(new KioskModeOperation(logger, settingsRepository)); } internal void LogStartupInformation() diff --git a/SafeExamBrowser.UserInterface.Classic/BrowserWindow.xaml.cs b/SafeExamBrowser.UserInterface.Classic/BrowserWindow.xaml.cs index 22e33d58..fd0d9bb2 100644 --- a/SafeExamBrowser.UserInterface.Classic/BrowserWindow.xaml.cs +++ b/SafeExamBrowser.UserInterface.Classic/BrowserWindow.xaml.cs @@ -55,12 +55,30 @@ namespace SafeExamBrowser.UserInterface.Classic public void BringToForeground() { - if (WindowState == WindowState.Minimized) + Dispatcher.Invoke(() => { - WindowState = WindowState.Normal; - } + if (WindowState == WindowState.Minimized) + { + WindowState = WindowState.Normal; + } - Activate(); + Activate(); + }); + } + + public new void Close() + { + Dispatcher.Invoke(base.Close); + } + + public new void Hide() + { + Dispatcher.Invoke(base.Hide); + } + + public new void Show() + { + Dispatcher.Invoke(base.Show); } public void UpdateAddress(string url) diff --git a/SafeExamBrowser.UserInterface.Classic/LogWindow.xaml.cs b/SafeExamBrowser.UserInterface.Classic/LogWindow.xaml.cs index ddf013f0..49d3f9a4 100644 --- a/SafeExamBrowser.UserInterface.Classic/LogWindow.xaml.cs +++ b/SafeExamBrowser.UserInterface.Classic/LogWindow.xaml.cs @@ -47,6 +47,11 @@ namespace SafeExamBrowser.UserInterface.Classic Dispatcher.Invoke(base.Close); } + public new void Hide() + { + Dispatcher.Invoke(base.Hide); + } + public new void Show() { Dispatcher.Invoke(base.Show); diff --git a/SafeExamBrowser.UserInterface.Classic/RuntimeWindow.xaml b/SafeExamBrowser.UserInterface.Classic/RuntimeWindow.xaml index 1964c83a..e443bf17 100644 --- a/SafeExamBrowser.UserInterface.Classic/RuntimeWindow.xaml +++ b/SafeExamBrowser.UserInterface.Classic/RuntimeWindow.xaml @@ -8,7 +8,7 @@ mc:Ignorable="d" Background="White" Foreground="White" Height="500" Width="750" WindowStyle="None" WindowStartupLocation="CenterScreen" Icon="./Images/SafeExamBrowser.ico" ResizeMode="NoResize" Title="Safe Exam Browser" Topmost="True"> - + @@ -18,7 +18,7 @@ - + @@ -41,8 +41,8 @@ - - + + diff --git a/SafeExamBrowser.UserInterface.Classic/RuntimeWindow.xaml.cs b/SafeExamBrowser.UserInterface.Classic/RuntimeWindow.xaml.cs index 948211e9..f2f25a1a 100644 --- a/SafeExamBrowser.UserInterface.Classic/RuntimeWindow.xaml.cs +++ b/SafeExamBrowser.UserInterface.Classic/RuntimeWindow.xaml.cs @@ -9,18 +9,22 @@ using System; using System.Windows; using System.Windows.Documents; +using System.Windows.Input; using SafeExamBrowser.Contracts.Configuration; using SafeExamBrowser.Contracts.I18n; using SafeExamBrowser.Contracts.Logging; using SafeExamBrowser.Contracts.UserInterface; +using SafeExamBrowser.UserInterface.Classic.ViewModels; namespace SafeExamBrowser.UserInterface.Classic { - public partial class RuntimeWindow : Window, ILogObserver, IRuntimeWindow + public partial class RuntimeWindow : Window, IRuntimeWindow { + private bool allowClose; private ILogContentFormatter formatter; private IRuntimeInfo runtimeInfo; private IText text; + private RuntimeWindowViewModel model; private WindowClosingEventHandler closing; event WindowClosingEventHandler IWindow.Closing @@ -44,6 +48,20 @@ namespace SafeExamBrowser.UserInterface.Classic Dispatcher.Invoke(Activate); } + public new void Close() + { + Dispatcher.Invoke(() => + { + allowClose = true; + base.Close(); + }); + } + + public new void Hide() + { + Dispatcher.Invoke(base.Hide); + } + public void Notify(ILogContent content) { Dispatcher.Invoke(() => @@ -53,18 +71,41 @@ namespace SafeExamBrowser.UserInterface.Classic }); } - public void UpdateStatus(TextKey key) + public new void Show() { - Dispatcher.Invoke(() => StatusTextBlock.Text = text.Get(key)); + Dispatcher.Invoke(base.Show); + } + + public void UpdateStatus(TextKey key, bool showBusyIndication = false) + { + Dispatcher.Invoke(() => + { + AnimatedBorder.Visibility = showBusyIndication ? Visibility.Hidden : Visibility.Visible; + ProgressBar.Visibility = showBusyIndication ? Visibility.Visible : Visibility.Hidden; + + model.StopBusyIndication(); + model.Status = text.Get(key); + + if (showBusyIndication) + { + model.StartBusyIndication(); + } + }); } private void InitializeRuntimeWindow() { Title = $"{runtimeInfo.ProgramTitle} - Version {runtimeInfo.ProgramVersion}"; + InfoTextBlock.Inlines.Add(new Run($"Version {runtimeInfo.ProgramVersion}") { FontStyle = FontStyles.Italic }); InfoTextBlock.Inlines.Add(new LineBreak()); InfoTextBlock.Inlines.Add(new LineBreak()); InfoTextBlock.Inlines.Add(new Run(runtimeInfo.ProgramCopyright) { FontSize = 10 }); + + model = new RuntimeWindowViewModel(); + StatusTextBlock.DataContext = model; + + Closing += (o, args) => args.Cancel = !allowClose; } } } diff --git a/SafeExamBrowser.UserInterface.Classic/SafeExamBrowser.UserInterface.Classic.csproj b/SafeExamBrowser.UserInterface.Classic/SafeExamBrowser.UserInterface.Classic.csproj index 9a743f2c..2d493715 100644 --- a/SafeExamBrowser.UserInterface.Classic/SafeExamBrowser.UserInterface.Classic.csproj +++ b/SafeExamBrowser.UserInterface.Classic/SafeExamBrowser.UserInterface.Classic.csproj @@ -113,10 +113,12 @@ + + Designer diff --git a/SafeExamBrowser.UserInterface.Classic/SplashScreen.xaml.cs b/SafeExamBrowser.UserInterface.Classic/SplashScreen.xaml.cs index da9104a4..ab135041 100644 --- a/SafeExamBrowser.UserInterface.Classic/SplashScreen.xaml.cs +++ b/SafeExamBrowser.UserInterface.Classic/SplashScreen.xaml.cs @@ -17,9 +17,17 @@ namespace SafeExamBrowser.UserInterface.Classic { public partial class SplashScreen : Window, ISplashScreen { + private bool allowClose; private SplashScreenViewModel model = new SplashScreenViewModel(); private IRuntimeInfo runtimeInfo; private IText text; + private WindowClosingEventHandler closing; + + event WindowClosingEventHandler IWindow.Closing + { + add { closing += value; } + remove { closing -= value; } + } public SplashScreen(IRuntimeInfo runtimeInfo, IText text) { @@ -30,14 +38,28 @@ namespace SafeExamBrowser.UserInterface.Classic InitializeSplashScreen(); } - public void InvokeClose() + public void BringToForeground() { - Dispatcher.Invoke(Close); + Dispatcher.Invoke(Activate); } - public void InvokeShow() + public new void Close() { - Dispatcher.Invoke(Show); + Dispatcher.Invoke(() => + { + allowClose = true; + base.Close(); + }); + } + + public new void Hide() + { + Dispatcher.Invoke(base.Hide); + } + + public new void Show() + { + Dispatcher.Invoke(base.Show); } public void Progress(int amount = 1) @@ -83,6 +105,8 @@ namespace SafeExamBrowser.UserInterface.Classic // To prevent the progress bar going from max to min value at startup... model.MaxProgress = 1; + + Closing += (o, args) => args.Cancel = !allowClose; } } } diff --git a/SafeExamBrowser.UserInterface.Classic/UserInterfaceFactory.cs b/SafeExamBrowser.UserInterface.Classic/UserInterfaceFactory.cs index 961d7b35..ae3863fd 100644 --- a/SafeExamBrowser.UserInterface.Classic/UserInterfaceFactory.cs +++ b/SafeExamBrowser.UserInterface.Classic/UserInterfaceFactory.cs @@ -15,6 +15,7 @@ using SafeExamBrowser.Contracts.Logging; using SafeExamBrowser.Contracts.UserInterface; using SafeExamBrowser.Contracts.UserInterface.Taskbar; using SafeExamBrowser.UserInterface.Classic.Controls; +using SafeExamBrowser.UserInterface.Classic.Utilities; using MessageBoxResult = SafeExamBrowser.Contracts.UserInterface.MessageBoxResult; namespace SafeExamBrowser.UserInterface.Classic @@ -76,6 +77,30 @@ namespace SafeExamBrowser.UserInterface.Classic return new PowerSupplyControl(); } + public IRuntimeWindow CreateRuntimeWindow(IRuntimeInfo runtimeInfo, IText text) + { + RuntimeWindow runtimeWindow = null; + var windowReadyEvent = new AutoResetEvent(false); + var runtimeWindowThread = new Thread(() => + { + runtimeWindow = new RuntimeWindow(new RuntimeWindowLogFormatter(), runtimeInfo, text); + runtimeWindow.Closed += (o, args) => runtimeWindow.Dispatcher.InvokeShutdown(); + + windowReadyEvent.Set(); + + System.Windows.Threading.Dispatcher.Run(); + }); + + runtimeWindowThread.SetApartmentState(ApartmentState.STA); + runtimeWindowThread.Name = nameof(RuntimeWindow); + runtimeWindowThread.IsBackground = true; + runtimeWindowThread.Start(); + + windowReadyEvent.WaitOne(); + + return runtimeWindow; + } + public ISplashScreen CreateSplashScreen(IRuntimeInfo runtimeInfo, IText text) { SplashScreen splashScreen = null; diff --git a/SafeExamBrowser.UserInterface.Classic/Utilities/RuntimeWindowLogFormatter.cs b/SafeExamBrowser.UserInterface.Classic/Utilities/RuntimeWindowLogFormatter.cs new file mode 100644 index 00000000..d7c49d06 --- /dev/null +++ b/SafeExamBrowser.UserInterface.Classic/Utilities/RuntimeWindowLogFormatter.cs @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2018 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; +using SafeExamBrowser.Contracts.Logging; + +namespace SafeExamBrowser.UserInterface.Classic.Utilities +{ + internal class RuntimeWindowLogFormatter : ILogContentFormatter + { + public string Format(ILogContent content) + { + if (content is ILogText) + { + return (content as ILogText).Text; + } + + if (content is ILogMessage) + { + return FormatLogMessage(content as ILogMessage); + } + + throw new NotImplementedException($"The runtime window formatter is not yet implemented for log content of type {content.GetType()}!"); + } + + private string FormatLogMessage(ILogMessage message) + { + var time = message.DateTime.ToString("HH:mm:ss.fff"); + var severity = message.Severity.ToString().ToUpper(); + + return $"{time} - {severity}: {message.Message}"; + } + } +} diff --git a/SafeExamBrowser.UserInterface.Classic/ViewModels/RuntimeWindowViewModel.cs b/SafeExamBrowser.UserInterface.Classic/ViewModels/RuntimeWindowViewModel.cs new file mode 100644 index 00000000..7c411cba --- /dev/null +++ b/SafeExamBrowser.UserInterface.Classic/ViewModels/RuntimeWindowViewModel.cs @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2018 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.ComponentModel; +using System.Timers; + +namespace SafeExamBrowser.UserInterface.Classic.ViewModels +{ + internal class RuntimeWindowViewModel : INotifyPropertyChanged + { + private string status; + private Timer timer; + + public event PropertyChangedEventHandler PropertyChanged; + + public string Status + { + get + { + return status; + } + set + { + status = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Status))); + } + } + + public void StartBusyIndication() + { + StopBusyIndication(); + + timer = new Timer + { + AutoReset = true, + Interval = 750 + }; + + timer.Elapsed += Timer_Elapsed; + timer.Start(); + } + + public void StopBusyIndication() + { + timer?.Stop(); + timer?.Close(); + } + + private void Timer_Elapsed(object sender, ElapsedEventArgs e) + { + var next = Status ?? string.Empty; + + if (next.EndsWith("...")) + { + next = Status.Substring(0, Status.Length - 3); + } + else + { + next += "."; + } + + Status = next; + } + } +} diff --git a/SafeExamBrowser.UserInterface.Windows10/BrowserWindow.xaml.cs b/SafeExamBrowser.UserInterface.Windows10/BrowserWindow.xaml.cs index 3bd37eab..f4022464 100644 --- a/SafeExamBrowser.UserInterface.Windows10/BrowserWindow.xaml.cs +++ b/SafeExamBrowser.UserInterface.Windows10/BrowserWindow.xaml.cs @@ -53,12 +53,30 @@ namespace SafeExamBrowser.UserInterface.Windows10 public void BringToForeground() { - if (WindowState == WindowState.Minimized) + Dispatcher.Invoke(() => { - WindowState = WindowState.Normal; - } + if (WindowState == WindowState.Minimized) + { + WindowState = WindowState.Normal; + } - Activate(); + Activate(); + }); + } + + public new void Close() + { + Dispatcher.Invoke(base.Close); + } + + public new void Hide() + { + Dispatcher.Invoke(base.Hide); + } + + public new void Show() + { + Dispatcher.Invoke(base.Show); } public void UpdateAddress(string url) diff --git a/SafeExamBrowser.UserInterface.Windows10/LogWindow.xaml.cs b/SafeExamBrowser.UserInterface.Windows10/LogWindow.xaml.cs index a2e1f815..d2530f17 100644 --- a/SafeExamBrowser.UserInterface.Windows10/LogWindow.xaml.cs +++ b/SafeExamBrowser.UserInterface.Windows10/LogWindow.xaml.cs @@ -47,6 +47,11 @@ namespace SafeExamBrowser.UserInterface.Windows10 Dispatcher.Invoke(base.Close); } + public new void Hide() + { + Dispatcher.Invoke(base.Hide); + } + public new void Show() { Dispatcher.Invoke(base.Show); diff --git a/SafeExamBrowser.UserInterface.Windows10/SplashScreen.xaml.cs b/SafeExamBrowser.UserInterface.Windows10/SplashScreen.xaml.cs index ff7b4e78..59bff6df 100644 --- a/SafeExamBrowser.UserInterface.Windows10/SplashScreen.xaml.cs +++ b/SafeExamBrowser.UserInterface.Windows10/SplashScreen.xaml.cs @@ -17,9 +17,17 @@ namespace SafeExamBrowser.UserInterface.Windows10 { public partial class SplashScreen : Window, ISplashScreen { + private bool allowClose; private SplashScreenViewModel model = new SplashScreenViewModel(); private IRuntimeInfo runtimeInfo; private IText text; + private WindowClosingEventHandler closing; + + event WindowClosingEventHandler IWindow.Closing + { + add { closing += value; } + remove { closing -= value; } + } public SplashScreen(IRuntimeInfo runtimeInfo, IText text) { @@ -30,14 +38,28 @@ namespace SafeExamBrowser.UserInterface.Windows10 InitializeSplashScreen(); } - public void InvokeClose() + public void BringToForeground() { - Dispatcher.Invoke(Close); + Dispatcher.Invoke(Activate); } - public void InvokeShow() + public new void Close() { - Dispatcher.Invoke(Show); + Dispatcher.Invoke(() => + { + allowClose = true; + base.Close(); + }); + } + + public new void Hide() + { + Dispatcher.Invoke(base.Hide); + } + + public new void Show() + { + Dispatcher.Invoke(base.Show); } public void Progress(int amount = 1) @@ -83,6 +105,8 @@ namespace SafeExamBrowser.UserInterface.Windows10 // To prevent the progress bar going from max to min value at startup... model.MaxProgress = 1; + + Closing += (o, args) => args.Cancel = !allowClose; } } } diff --git a/SafeExamBrowser.UserInterface.Windows10/UserInterfaceFactory.cs b/SafeExamBrowser.UserInterface.Windows10/UserInterfaceFactory.cs index 75f1ef37..ae4ceae9 100644 --- a/SafeExamBrowser.UserInterface.Windows10/UserInterfaceFactory.cs +++ b/SafeExamBrowser.UserInterface.Windows10/UserInterfaceFactory.cs @@ -77,6 +77,12 @@ namespace SafeExamBrowser.UserInterface.Windows10 return new PowerSupplyControl(); } + public IRuntimeWindow CreateRuntimeWindow(IRuntimeInfo runtimeInfo, IText text) + { + // TODO + throw new System.NotImplementedException(); + } + public ISplashScreen CreateSplashScreen(IRuntimeInfo runtimeInfo, IText text) { SplashScreen splashScreen = null;