diff --git a/SafeExamBrowser.Client/CompositionRoot.cs b/SafeExamBrowser.Client/CompositionRoot.cs
index a5b11842..5cb68264 100644
--- a/SafeExamBrowser.Client/CompositionRoot.cs
+++ b/SafeExamBrowser.Client/CompositionRoot.cs
@@ -23,6 +23,7 @@ using SafeExamBrowser.Contracts.Client;
using SafeExamBrowser.Contracts.Communication.Hosts;
using SafeExamBrowser.Contracts.Communication.Proxies;
using SafeExamBrowser.Contracts.Configuration;
+using SafeExamBrowser.Contracts.Configuration.Settings;
using SafeExamBrowser.Contracts.Core.OperationModel;
using SafeExamBrowser.Contracts.I18n;
using SafeExamBrowser.Contracts.Logging;
@@ -42,7 +43,6 @@ using SafeExamBrowser.Monitoring.Mouse;
using SafeExamBrowser.Monitoring.Processes;
using SafeExamBrowser.Monitoring.Windows;
using SafeExamBrowser.SystemComponents;
-using SafeExamBrowser.UserInterface.Desktop;
using SafeExamBrowser.WindowsApi;
namespace SafeExamBrowser.Client
@@ -54,6 +54,7 @@ namespace SafeExamBrowser.Client
private LogLevel logLevel;
private string runtimeHostUri;
private Guid startupToken;
+ private UserInterfaceMode uiMode;
private IActionCenter actionCenter;
private IBrowserApplicationController browserController;
@@ -87,14 +88,14 @@ namespace SafeExamBrowser.Client
InitializeLogging();
InitializeText();
- actionCenter = new ActionCenter();
+ actionCenter = BuildActionCenter();
keyboardLayout = new KeyboardLayout(new ModuleLogger(logger, nameof(KeyboardLayout)), text);
- messageBox = new MessageBox(text);
+ messageBox = BuildMessageBox();
powerSupply = new PowerSupply(new ModuleLogger(logger, nameof(PowerSupply)), text);
processMonitor = new ProcessMonitor(new ModuleLogger(logger, nameof(ProcessMonitor)), nativeMethods);
- uiFactory = new UserInterfaceFactory(text);
+ uiFactory = BuildUserInterfaceFactory();
runtimeProxy = new RuntimeProxy(runtimeHostUri, new ProxyObjectFactory(), new ModuleLogger(logger, nameof(RuntimeProxy)));
- taskbar = new Taskbar(new ModuleLogger(logger, nameof(Taskbar)));
+ taskbar = BuildTaskbar();
windowMonitor = new WindowMonitor(new ModuleLogger(logger, nameof(WindowMonitor)), nativeMethods);
wirelessNetwork = new WirelessNetwork(new ModuleLogger(logger, nameof(WirelessNetwork)), text);
@@ -153,7 +154,7 @@ namespace SafeExamBrowser.Client
private void ValidateCommandLineArguments()
{
var args = Environment.GetCommandLineArgs();
- var hasFive = args?.Length == 5;
+ var hasFive = args?.Length >= 5;
if (hasFive)
{
@@ -168,6 +169,7 @@ namespace SafeExamBrowser.Client
logLevel = level;
runtimeHostUri = args[3];
startupToken = token;
+ uiMode = args.Length == 6 && Enum.TryParse(args[5], out uiMode) ? uiMode : UserInterfaceMode.Desktop;
return;
}
@@ -286,6 +288,50 @@ namespace SafeExamBrowser.Client
return new WindowMonitorOperation(configuration.Settings.KioskMode, logger, windowMonitor);
}
+ private IActionCenter BuildActionCenter()
+ {
+ switch (uiMode)
+ {
+ case UserInterfaceMode.Mobile:
+ return new UserInterface.Mobile.ActionCenter();
+ default:
+ return new UserInterface.Desktop.ActionCenter();
+ }
+ }
+
+ private IMessageBox BuildMessageBox()
+ {
+ switch (uiMode)
+ {
+ case UserInterfaceMode.Mobile:
+ return new UserInterface.Mobile.MessageBox(text);
+ default:
+ return new UserInterface.Desktop.MessageBox(text);
+ }
+ }
+
+ private ITaskbar BuildTaskbar()
+ {
+ switch (uiMode)
+ {
+ case UserInterfaceMode.Mobile:
+ return new UserInterface.Mobile.Taskbar(new ModuleLogger(logger, nameof(UserInterface.Mobile.Taskbar)));
+ default:
+ return new UserInterface.Desktop.Taskbar(new ModuleLogger(logger, nameof(UserInterface.Desktop.Taskbar)));
+ }
+ }
+
+ private IUserInterfaceFactory BuildUserInterfaceFactory()
+ {
+ switch (uiMode)
+ {
+ case UserInterfaceMode.Mobile:
+ return new UserInterface.Mobile.UserInterfaceFactory(text);
+ default:
+ return new UserInterface.Desktop.UserInterfaceFactory(text);
+ }
+ }
+
private void UpdateAppConfig()
{
ClientController.AppConfig = configuration.AppConfig;
diff --git a/SafeExamBrowser.Client/SafeExamBrowser.Client.csproj b/SafeExamBrowser.Client/SafeExamBrowser.Client.csproj
index 23c1b104..fc59b7b9 100644
--- a/SafeExamBrowser.Client/SafeExamBrowser.Client.csproj
+++ b/SafeExamBrowser.Client/SafeExamBrowser.Client.csproj
@@ -160,6 +160,10 @@
{A502DF54-7169-4647-94BD-18B192924866}
SafeExamBrowser.UserInterface.Desktop
+
+ {89bc24dd-ff31-496e-9816-a160b686a3d4}
+ SafeExamBrowser.UserInterface.Mobile
+
{73724659-4150-4792-A94E-42F5F3C1B696}
SafeExamBrowser.WindowsApi
diff --git a/SafeExamBrowser.Configuration/ConfigurationData/DataValues.cs b/SafeExamBrowser.Configuration/ConfigurationData/DataValues.cs
index a9a3b9ec..43d5ad77 100644
--- a/SafeExamBrowser.Configuration/ConfigurationData/DataValues.cs
+++ b/SafeExamBrowser.Configuration/ConfigurationData/DataValues.cs
@@ -148,6 +148,8 @@ namespace SafeExamBrowser.Configuration.ConfigurationData
settings.Taskbar.EnableTaskbar = true;
settings.Taskbar.ShowClock = true;
+ settings.UserInterfaceMode = UserInterfaceMode.Desktop;
+
// TODO: Default values for testing of alpha version only, remove for final release!
settings.ActionCenter.ShowApplicationLog = true;
settings.ActionCenter.ShowKeyboardLayout = true;
diff --git a/SafeExamBrowser.Runtime/Operations/ClientOperation.cs b/SafeExamBrowser.Runtime/Operations/ClientOperation.cs
index ff46d4d0..a56921e8 100644
--- a/SafeExamBrowser.Runtime/Operations/ClientOperation.cs
+++ b/SafeExamBrowser.Runtime/Operations/ClientOperation.cs
@@ -105,11 +105,12 @@ namespace SafeExamBrowser.Runtime.Operations
var clientLogLevel = Context.Next.Settings.LogLevel.ToString();
var runtimeHostUri = Context.Next.AppConfig.RuntimeAddress;
var startupToken = Context.Next.StartupToken.ToString("D");
+ var uiMode = Context.Next.Settings.UserInterfaceMode.ToString();
logger.Info("Starting new client process...");
runtimeHost.AllowConnection = true;
runtimeHost.ClientReady += clientReadyEventHandler;
- ClientProcess = processFactory.StartNew(clientExecutable, clientLogFile, clientLogLevel, runtimeHostUri, startupToken);
+ ClientProcess = processFactory.StartNew(clientExecutable, clientLogFile, clientLogLevel, runtimeHostUri, startupToken, uiMode);
logger.Info("Waiting for client to complete initialization...");
clientReady = clientReadyEvent.WaitOne(timeout_ms);
diff --git a/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenterApplicationControl.xaml.cs b/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenterApplicationControl.xaml.cs
index 25526b32..d4c577c9 100644
--- a/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenterApplicationControl.xaml.cs
+++ b/SafeExamBrowser.UserInterface.Desktop/Controls/ActionCenterApplicationControl.xaml.cs
@@ -11,7 +11,6 @@ using System.Windows.Controls;
using SafeExamBrowser.Contracts.Applications;
using SafeExamBrowser.Contracts.UserInterface.Shell;
using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
-using SafeExamBrowser.UserInterface.Desktop.Utilities;
namespace SafeExamBrowser.UserInterface.Desktop.Controls
{
diff --git a/SafeExamBrowser.UserInterface.Desktop/Images/Chromium.ico b/SafeExamBrowser.UserInterface.Desktop/Images/Chromium.ico
deleted file mode 100644
index 46025b80..00000000
Binary files a/SafeExamBrowser.UserInterface.Desktop/Images/Chromium.ico and /dev/null differ
diff --git a/SafeExamBrowser.UserInterface.Desktop/Properties/Resources.Designer.cs b/SafeExamBrowser.UserInterface.Desktop/Properties/Resources.Designer.cs
deleted file mode 100644
index fd280764..00000000
--- a/SafeExamBrowser.UserInterface.Desktop/Properties/Resources.Designer.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-//------------------------------------------------------------------------------
-//
-// This code was generated by a tool.
-// Runtime Version:4.0.30319.42000
-//
-// Changes to this file may cause incorrect behavior and will be lost if
-// the code is regenerated.
-//
-//------------------------------------------------------------------------------
-
-namespace SafeExamBrowser.UserInterface.Desktop.Properties {
- using System;
-
-
- ///
- /// A strongly-typed resource class, for looking up localized strings, etc.
- ///
- // This class was auto-generated by the StronglyTypedResourceBuilder
- // class via a tool like ResGen or Visual Studio.
- // To add or remove a member, edit your .ResX file then rerun ResGen
- // with the /str option, or rebuild your VS project.
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
- [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
- [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
- internal class Resources {
-
- private static global::System.Resources.ResourceManager resourceMan;
-
- private static global::System.Globalization.CultureInfo resourceCulture;
-
- [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
- internal Resources() {
- }
-
- ///
- /// Returns the cached ResourceManager instance used by this class.
- ///
- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Resources.ResourceManager ResourceManager {
- get {
- if (object.ReferenceEquals(resourceMan, null)) {
- global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SafeExamBrowser.UserInterface.Desktop.Properties.Resources", typeof(Resources).Assembly);
- resourceMan = temp;
- }
- return resourceMan;
- }
- }
-
- ///
- /// Overrides the current thread's CurrentUICulture property for all
- /// resource lookups using this strongly typed resource class.
- ///
- [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static global::System.Globalization.CultureInfo Culture {
- get {
- return resourceCulture;
- }
- set {
- resourceCulture = value;
- }
- }
- }
-}
diff --git a/SafeExamBrowser.UserInterface.Desktop/Properties/Resources.resx b/SafeExamBrowser.UserInterface.Desktop/Properties/Resources.resx
deleted file mode 100644
index af7dbebb..00000000
--- a/SafeExamBrowser.UserInterface.Desktop/Properties/Resources.resx
+++ /dev/null
@@ -1,117 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- text/microsoft-resx
-
-
- 2.0
-
-
- System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
- System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Desktop/Properties/Settings.Designer.cs b/SafeExamBrowser.UserInterface.Desktop/Properties/Settings.Designer.cs
deleted file mode 100644
index a6f031d0..00000000
--- a/SafeExamBrowser.UserInterface.Desktop/Properties/Settings.Designer.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-//------------------------------------------------------------------------------
-//
-// This code was generated by a tool.
-// Runtime Version:4.0.30319.42000
-//
-// Changes to this file may cause incorrect behavior and will be lost if
-// the code is regenerated.
-//
-//------------------------------------------------------------------------------
-
-namespace SafeExamBrowser.UserInterface.Desktop.Properties {
-
-
- [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.9.0.0")]
- internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
-
- private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
-
- public static Settings Default {
- get {
- return defaultInstance;
- }
- }
- }
-}
diff --git a/SafeExamBrowser.UserInterface.Desktop/Properties/Settings.settings b/SafeExamBrowser.UserInterface.Desktop/Properties/Settings.settings
deleted file mode 100644
index 033d7a5e..00000000
--- a/SafeExamBrowser.UserInterface.Desktop/Properties/Settings.settings
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj b/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj
index 2d223ea0..aea4f425 100644
--- a/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj
+++ b/SafeExamBrowser.UserInterface.Desktop/SafeExamBrowser.UserInterface.Desktop.csproj
@@ -364,27 +364,9 @@
Code
-
- True
- True
- Resources.resx
-
-
- True
- Settings.settings
- True
-
-
- ResXFileCodeGenerator
- Resources.Designer.cs
-
Designer
-
- SettingsSingleFileGenerator
- Settings.Designer.cs
-
@@ -392,9 +374,6 @@
SafeExamBrowser.Contracts
-
-
-
diff --git a/SafeExamBrowser.UserInterface.Mobile/AboutWindow.xaml b/SafeExamBrowser.UserInterface.Mobile/AboutWindow.xaml
new file mode 100644
index 00000000..88cccf7e
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/AboutWindow.xaml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This application is subject to the terms of the Mozilla Public License, version 2.0. If a copy of the MPL was not
+ distributed with this application, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+ CefSharp (.NET bindings for the Chromium Embedded Framework)
+
+ Copyright © 2010-2019 The CefSharp Authors. All rights reserved.
+
+
+ CEF (Chromium Embedded Framework)
+
+ Copyright © 2008-2019 Marshall A. Greenblatt. Portions Copyright © 2006-2009 Google Inc. All rights reserved.
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/AboutWindow.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/AboutWindow.xaml.cs
new file mode 100644
index 00000000..bc3f2420
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/AboutWindow.xaml.cs
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2019 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;
+using System.Windows.Documents;
+using SafeExamBrowser.Contracts.Configuration;
+using SafeExamBrowser.Contracts.I18n;
+using SafeExamBrowser.Contracts.UserInterface.Windows;
+using SafeExamBrowser.Contracts.UserInterface.Windows.Events;
+
+namespace SafeExamBrowser.UserInterface.Mobile
+{
+ public partial class AboutWindow : Window, IWindow
+ {
+ private AppConfig appConfig;
+ private IText text;
+ private WindowClosingEventHandler closing;
+
+ event WindowClosingEventHandler IWindow.Closing
+ {
+ add { closing += value; }
+ remove { closing -= value; }
+ }
+
+ public AboutWindow(AppConfig appConfig, IText text)
+ {
+ this.appConfig = appConfig;
+ this.text = text;
+
+ InitializeComponent();
+ InitializeAboutWindow();
+ }
+
+ public void BringToForeground()
+ {
+ Activate();
+ }
+
+ private void InitializeAboutWindow()
+ {
+ Closing += (o, args) => closing?.Invoke();
+ VersionInfo.Inlines.Add(new Run($"{text.Get(TextKey.Version)} {appConfig.ProgramVersion}") { FontStyle = FontStyles.Italic });
+ VersionInfo.Inlines.Add(new LineBreak());
+ VersionInfo.Inlines.Add(new LineBreak());
+ VersionInfo.Inlines.Add(new Run(appConfig.ProgramCopyright) { FontSize = 10 });
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/ActionCenter.xaml b/SafeExamBrowser.UserInterface.Mobile/ActionCenter.xaml
new file mode 100644
index 00000000..83088909
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/ActionCenter.xaml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/ActionCenter.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/ActionCenter.xaml.cs
new file mode 100644
index 00000000..df54e78a
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/ActionCenter.xaml.cs
@@ -0,0 +1,195 @@
+/*
+ * Copyright (c) 2019 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 System.Windows;
+using System.Windows.Media.Animation;
+using SafeExamBrowser.Contracts.I18n;
+using SafeExamBrowser.Contracts.UserInterface.Shell;
+using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
+
+namespace SafeExamBrowser.UserInterface.Mobile
+{
+ public partial class ActionCenter : Window, IActionCenter
+ {
+ public bool ShowClock
+ {
+ set { Dispatcher.Invoke(() => Clock.Visibility = value ? Visibility.Visible : Visibility.Collapsed); }
+ }
+
+ public event QuitButtonClickedEventHandler QuitButtonClicked;
+
+ public ActionCenter()
+ {
+ InitializeComponent();
+ InitializeActionCenter();
+ }
+
+ public void AddApplicationControl(IApplicationControl control)
+ {
+ if (control is UIElement uiElement)
+ {
+ ApplicationPanel.Children.Add(uiElement);
+ }
+ }
+
+ public void AddNotificationControl(INotificationControl control)
+ {
+ if (control is UIElement uiElement)
+ {
+ ControlPanel.Children.Insert(ControlPanel.Children.Count - 2, uiElement);
+ }
+ }
+
+ public void AddSystemControl(ISystemControl control)
+ {
+ if (control is UIElement uiElement)
+ {
+ ControlPanel.Children.Insert(ControlPanel.Children.Count - 2, uiElement);
+ }
+ }
+
+ public new void Close()
+ {
+ Dispatcher.Invoke(base.Close);
+ }
+
+ public new void Hide()
+ {
+ Dispatcher.Invoke(HideAnimated);
+ }
+
+ public void InitializeBounds()
+ {
+ Dispatcher.Invoke(() =>
+ {
+ Height = SystemParameters.WorkArea.Height;
+ Top = 0;
+ Left = -Width;
+ });
+ }
+
+ public void InitializeText(IText text)
+ {
+ QuitButton.ToolTip = text.Get(TextKey.Shell_QuitButton);
+ QuitButton.Text.Text = text.Get(TextKey.Shell_QuitButton);
+ }
+
+ public void Register(IActionCenterActivator activator)
+ {
+ activator.Activate += Activator_Activate;
+ activator.Deactivate += Activator_Deactivate;
+ activator.Toggle += Activator_Toggle;
+ }
+
+ public new void Show()
+ {
+ Dispatcher.Invoke(ShowAnimated);
+ }
+
+ private void HideAnimated()
+ {
+ var storyboard = new Storyboard();
+ var animation = new DoubleAnimation
+ {
+ EasingFunction = new CircleEase { EasingMode = EasingMode.EaseOut },
+ From = 0,
+ To = -Width,
+ Duration = new Duration(TimeSpan.FromMilliseconds(500))
+ };
+
+ Storyboard.SetTarget(animation, this);
+ Storyboard.SetTargetProperty(animation, new PropertyPath(LeftProperty));
+
+ storyboard.Children.Add(animation);
+ storyboard.Completed += HideAnimation_Completed;
+ storyboard.Begin();
+ }
+
+ private void ShowAnimated()
+ {
+ var storyboard = new Storyboard();
+ var animation = new DoubleAnimation
+ {
+ EasingFunction = new CircleEase { EasingMode = EasingMode.EaseOut },
+ From = -Width,
+ To = 0,
+ Duration = new Duration(TimeSpan.FromMilliseconds(500))
+ };
+
+ Storyboard.SetTarget(animation, this);
+ Storyboard.SetTargetProperty(animation, new PropertyPath(LeftProperty));
+
+ InitializeBounds();
+ base.Show();
+
+ storyboard.Children.Add(animation);
+ storyboard.Completed += ShowAnimation_Completed;
+ storyboard.Begin();
+ }
+
+ private void ShowAnimation_Completed(object sender, EventArgs e)
+ {
+ Activate();
+ Deactivated += ActionCenter_Deactivated;
+ }
+
+ private void HideAnimation_Completed(object sender, EventArgs e)
+ {
+ Deactivated -= ActionCenter_Deactivated;
+ base.Hide();
+ }
+
+ private void ActionCenter_Deactivated(object sender, EventArgs e)
+ {
+ HideAnimated();
+ }
+
+ private void Activator_Activate()
+ {
+ Dispatcher.InvokeAsync(() =>
+ {
+ if (Visibility != Visibility.Visible)
+ {
+ ShowAnimated();
+ }
+ });
+ }
+
+ private void Activator_Deactivate()
+ {
+ Dispatcher.InvokeAsync(() =>
+ {
+ if (Visibility == Visibility.Visible)
+ {
+ HideAnimated();
+ }
+ });
+ }
+
+ private void Activator_Toggle()
+ {
+ Dispatcher.InvokeAsync(() =>
+ {
+ if (Visibility != Visibility.Visible)
+ {
+ ShowAnimated();
+ }
+ else
+ {
+ HideAnimated();
+ }
+ });
+ }
+
+ private void InitializeActionCenter()
+ {
+ QuitButton.Clicked += (args) => QuitButtonClicked?.Invoke(args);
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/BrowserWindow.xaml b/SafeExamBrowser.UserInterface.Mobile/BrowserWindow.xaml
new file mode 100644
index 00000000..bdc6ece4
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/BrowserWindow.xaml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/BrowserWindow.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/BrowserWindow.xaml.cs
new file mode 100644
index 00000000..812e4240
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/BrowserWindow.xaml.cs
@@ -0,0 +1,282 @@
+/*
+ * Copyright (c) 2019 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 System.ComponentModel;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls.Primitives;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using SafeExamBrowser.Contracts.Configuration.Settings;
+using SafeExamBrowser.Contracts.Core;
+using SafeExamBrowser.Contracts.I18n;
+using SafeExamBrowser.Contracts.UserInterface;
+using SafeExamBrowser.Contracts.UserInterface.Browser;
+using SafeExamBrowser.Contracts.UserInterface.Browser.Events;
+using SafeExamBrowser.Contracts.UserInterface.Windows;
+using SafeExamBrowser.Contracts.UserInterface.Windows.Events;
+using SafeExamBrowser.UserInterface.Mobile.Utilities;
+
+namespace SafeExamBrowser.UserInterface.Mobile
+{
+ public partial class BrowserWindow : Window, IBrowserWindow
+ {
+ private bool isMainWindow;
+ private BrowserSettings settings;
+ private IText text;
+ private WindowClosingEventHandler closing;
+
+ private BrowserWindowSettings WindowSettings
+ {
+ get { return isMainWindow ? settings.MainWindowSettings : settings.AdditionalWindowSettings; }
+ }
+
+ public bool CanNavigateBackwards { set => Dispatcher.Invoke(() => BackwardButton.IsEnabled = value); }
+ public bool CanNavigateForwards { set => Dispatcher.Invoke(() => ForwardButton.IsEnabled = value); }
+
+ public event AddressChangedEventHandler AddressChanged;
+ public event ActionRequestedEventHandler BackwardNavigationRequested;
+ public event ActionRequestedEventHandler ForwardNavigationRequested;
+ public event ActionRequestedEventHandler ReloadRequested;
+ public event ActionRequestedEventHandler ZoomInRequested;
+ public event ActionRequestedEventHandler ZoomOutRequested;
+ public event ActionRequestedEventHandler ZoomResetRequested;
+
+ event WindowClosingEventHandler IWindow.Closing
+ {
+ add { closing += value; }
+ remove { closing -= value; }
+ }
+
+ public BrowserWindow(IBrowserControl browserControl, BrowserSettings settings, bool isMainWindow, IText text)
+ {
+ this.isMainWindow = isMainWindow;
+ this.settings = settings;
+ this.text = text;
+
+ InitializeComponent();
+ InitializeBrowserWindow(browserControl);
+ }
+
+ public void BringToForeground()
+ {
+ Dispatcher.Invoke(() =>
+ {
+ if (WindowState == WindowState.Minimized)
+ {
+ WindowState = WindowState.Normal;
+ }
+
+ Activate();
+ });
+ }
+
+ public new void Close()
+ {
+ Dispatcher.Invoke(() =>
+ {
+ Closing -= BrowserWindow_Closing;
+ closing?.Invoke();
+ base.Close();
+ });
+ }
+
+ public new void Hide()
+ {
+ Dispatcher.Invoke(base.Hide);
+ }
+
+ public new void Show()
+ {
+ Dispatcher.Invoke(base.Show);
+ }
+
+ public void UpdateAddress(string url)
+ {
+ Dispatcher.Invoke(() => UrlTextBox.Text = url);
+ }
+
+ public void UpdateIcon(IIconResource icon)
+ {
+ Dispatcher.InvokeAsync(() => Icon = new BitmapImage(icon.Uri));
+ }
+
+ public void UpdateLoadingState(bool isLoading)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ LoadingIcon.Visibility = isLoading ? Visibility.Visible : Visibility.Collapsed;
+ LoadingIcon.Spin = isLoading;
+ });
+ }
+
+ public void UpdateTitle(string title)
+ {
+ Dispatcher.Invoke(() => Title = title);
+ }
+
+ private void BrowserWindow_Closing(object sender, CancelEventArgs e)
+ {
+ if (isMainWindow)
+ {
+ e.Cancel = true;
+ }
+ else
+ {
+ closing?.Invoke();
+ }
+ }
+
+ private void BrowserWindow_KeyUp(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.F5)
+ {
+ ReloadRequested?.Invoke();
+ }
+ }
+
+ private CustomPopupPlacement[] MenuPopup_PlacementCallback(Size popupSize, Size targetSize, Point offset)
+ {
+ return new[]
+ {
+ new CustomPopupPlacement(new Point(targetSize.Width - Toolbar.Margin.Right - popupSize.Width, -2), PopupPrimaryAxis.None)
+ };
+ }
+
+ private void SystemParameters_StaticPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(SystemParameters.WorkArea))
+ {
+ Dispatcher.InvokeAsync(InitializeBounds);
+ }
+ }
+
+ private void UrlTextBox_GotMouseCapture(object sender, MouseEventArgs e)
+ {
+ if (UrlTextBox.Tag as bool? != true)
+ {
+ UrlTextBox.SelectAll();
+ UrlTextBox.Tag = true;
+ }
+ }
+
+ private void UrlTextBox_KeyUp(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Enter)
+ {
+ AddressChanged?.Invoke(UrlTextBox.Text);
+ }
+ }
+
+ private void InitializeBrowserWindow(IBrowserControl browserControl)
+ {
+ if (browserControl is System.Windows.Forms.Control control)
+ {
+ BrowserControlHost.Child = control;
+ }
+
+ RegisterEvents();
+ InitializeBounds();
+ ApplySettings();
+ LoadIcons();
+ LoadText();
+ }
+
+ private void RegisterEvents()
+ {
+ var originalBrush = MenuButton.Background;
+
+ BackwardButton.Click += (o, args) => BackwardNavigationRequested?.Invoke();
+ Closing += BrowserWindow_Closing;
+ ForwardButton.Click += (o, args) => ForwardNavigationRequested?.Invoke();
+ MenuButton.Click += (o, args) => MenuPopup.IsOpen = !MenuPopup.IsOpen;
+ MenuButton.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => MenuPopup.IsOpen = MenuPopup.IsMouseOver));
+ MenuPopup.CustomPopupPlacementCallback = new CustomPopupPlacementCallback(MenuPopup_PlacementCallback);
+ MenuPopup.Closed += (o, args) => MenuButton.Background = originalBrush;
+ MenuPopup.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => MenuPopup.IsOpen = IsMouseOver));
+ MenuPopup.Opened += (o, args) => MenuButton.Background = Brushes.LightGray;
+ KeyUp += BrowserWindow_KeyUp;
+ ReloadButton.Click += (o, args) => ReloadRequested?.Invoke();
+ SystemParameters.StaticPropertyChanged += SystemParameters_StaticPropertyChanged;
+ UrlTextBox.GotKeyboardFocus += (o, args) => UrlTextBox.SelectAll();
+ UrlTextBox.GotMouseCapture += UrlTextBox_GotMouseCapture;
+ UrlTextBox.LostKeyboardFocus += (o, args) => UrlTextBox.Tag = null;
+ UrlTextBox.LostFocus += (o, args) => UrlTextBox.Tag = null;
+ UrlTextBox.KeyUp += UrlTextBox_KeyUp;
+ UrlTextBox.MouseDoubleClick += (o, args) => UrlTextBox.SelectAll();
+ ZoomInButton.Click += (o, args) => ZoomInRequested?.Invoke();
+ ZoomOutButton.Click += (o, args) => ZoomOutRequested?.Invoke();
+ ZoomResetButton.Click += (o, args) => ZoomResetRequested?.Invoke();
+ }
+
+ private void ApplySettings()
+ {
+ UrlTextBox.IsEnabled = WindowSettings.AllowAddressBar;
+
+ ReloadButton.IsEnabled = WindowSettings.AllowReloading;
+ ReloadButton.Visibility = WindowSettings.AllowReloading ? Visibility.Visible : Visibility.Collapsed;
+
+ BackwardButton.IsEnabled = WindowSettings.AllowBackwardNavigation;
+ BackwardButton.Visibility = WindowSettings.AllowBackwardNavigation ? Visibility.Visible : Visibility.Collapsed;
+
+ ForwardButton.IsEnabled = WindowSettings.AllowForwardNavigation;
+ ForwardButton.Visibility = WindowSettings.AllowForwardNavigation ? Visibility.Visible : Visibility.Collapsed;
+ }
+
+ private void InitializeBounds()
+ {
+ if (isMainWindow)
+ {
+ if (WindowSettings.FullScreenMode)
+ {
+ Top = 0;
+ Left = 0;
+ Height = SystemParameters.WorkArea.Height;
+ Width = SystemParameters.WorkArea.Width;
+ ResizeMode = ResizeMode.NoResize;
+ WindowStyle = WindowStyle.None;
+ }
+ else
+ {
+ WindowState = WindowState.Maximized;
+ }
+ }
+ else
+ {
+ Top = 0;
+ Left = SystemParameters.WorkArea.Width / 2;
+ Height = SystemParameters.WorkArea.Height;
+ Width = SystemParameters.WorkArea.Width / 2;
+ }
+ }
+
+ private void LoadIcons()
+ {
+ var backUri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/NavigateBack.xaml");
+ var forwardUri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/NavigateForward.xaml");
+ var menuUri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/Menu.xaml");
+ var reloadUri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/Reload.xaml");
+ var backward = new XamlIconResource(backUri);
+ var forward = new XamlIconResource(forwardUri);
+ var menu = new XamlIconResource(menuUri);
+ var reload = new XamlIconResource(reloadUri);
+
+ BackwardButton.Content = IconResourceLoader.Load(backward);
+ ForwardButton.Content = IconResourceLoader.Load(forward);
+ MenuButton.Content = IconResourceLoader.Load(menu);
+ ReloadButton.Content = IconResourceLoader.Load(reload);
+ }
+
+ private void LoadText()
+ {
+ ZoomText.Text = text.Get(TextKey.BrowserWindow_ZoomMenuItem);
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterApplicationButton.xaml b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterApplicationButton.xaml
new file mode 100644
index 00000000..00092feb
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterApplicationButton.xaml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterApplicationButton.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterApplicationButton.xaml.cs
new file mode 100644
index 00000000..044d4de7
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterApplicationButton.xaml.cs
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2019 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;
+using System.Windows.Controls;
+using SafeExamBrowser.Contracts.Applications;
+using SafeExamBrowser.Contracts.Core;
+using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
+using SafeExamBrowser.UserInterface.Mobile.Utilities;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Controls
+{
+ public partial class ActionCenterApplicationButton : UserControl
+ {
+ private IApplicationInfo info;
+ private IApplicationInstance instance;
+
+ internal event ApplicationControlClickedEventHandler Clicked;
+
+ public ActionCenterApplicationButton(IApplicationInfo info, IApplicationInstance instance = null)
+ {
+ this.info = info;
+ this.instance = instance;
+
+ InitializeComponent();
+ InitializeApplicationInstanceButton();
+ }
+
+ private void InitializeApplicationInstanceButton()
+ {
+ Icon.Content = IconResourceLoader.Load(info.IconResource);
+ Text.Text = instance?.Name ?? info.Name;
+ Button.ToolTip = instance?.Name ?? info.Tooltip;
+
+ if (instance != null)
+ {
+ instance.IconChanged += Instance_IconChanged;
+ instance.NameChanged += Instance_NameChanged;
+ }
+ }
+
+ private void Instance_IconChanged(IIconResource icon)
+ {
+ Dispatcher.InvokeAsync(() => Icon.Content = IconResourceLoader.Load(icon));
+ }
+
+ private void Instance_NameChanged(string name)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ Text.Text = name;
+ Button.ToolTip = name;
+ });
+ }
+
+ private void Button_Click(object sender, RoutedEventArgs e)
+ {
+ Clicked?.Invoke(instance?.Id);
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterApplicationControl.xaml b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterApplicationControl.xaml
new file mode 100644
index 00000000..0ffea172
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterApplicationControl.xaml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterApplicationControl.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterApplicationControl.xaml.cs
new file mode 100644
index 00000000..16a82641
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterApplicationControl.xaml.cs
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2019 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;
+using System.Windows.Controls;
+using SafeExamBrowser.Contracts.Applications;
+using SafeExamBrowser.Contracts.UserInterface.Shell;
+using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Controls
+{
+ public partial class ActionCenterApplicationControl : UserControl, IApplicationControl
+ {
+ private IApplicationInfo info;
+
+ public event ApplicationControlClickedEventHandler Clicked;
+
+ public ActionCenterApplicationControl(IApplicationInfo info)
+ {
+ this.info = info;
+
+ InitializeComponent();
+ InitializeApplicationControl(info);
+ }
+
+ public void RegisterInstance(IApplicationInstance instance)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ var button = new ActionCenterApplicationButton(info, instance);
+
+ button.Clicked += (id) => Clicked?.Invoke(id);
+ instance.Terminated += (id) => Instance_OnTerminated(id, button);
+ InstancePanel.Children.Add(button);
+
+ ApplicationName.Visibility = Visibility.Visible;
+ ApplicationButton.Visibility = Visibility.Collapsed;
+ });
+ }
+
+ private void InitializeApplicationControl(IApplicationInfo info)
+ {
+ var button = new ActionCenterApplicationButton(info);
+
+ button.Button.Click += (o, args) => Clicked?.Invoke();
+ ApplicationName.Text = info.Name;
+ ApplicationButton.Content = button;
+ }
+
+ private void Instance_OnTerminated(InstanceIdentifier id, ActionCenterApplicationButton button)
+ {
+ Dispatcher.InvokeAsync(() =>
+ {
+ InstancePanel.Children.Remove(button);
+
+ if (InstancePanel.Children.Count == 0)
+ {
+ ApplicationName.Visibility = Visibility.Collapsed;
+ ApplicationButton.Visibility = Visibility.Visible;
+ }
+ });
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterClock.xaml b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterClock.xaml
new file mode 100644
index 00000000..a881ffff
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterClock.xaml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterClock.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterClock.xaml.cs
new file mode 100644
index 00000000..adc709c1
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterClock.xaml.cs
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2019 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.Controls;
+using SafeExamBrowser.UserInterface.Mobile.ViewModels;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Controls
+{
+ public partial class ActionCenterClock : UserControl
+ {
+ private DateTimeViewModel model;
+
+ public ActionCenterClock()
+ {
+ InitializeComponent();
+ InitializeControl();
+ }
+
+ private void InitializeControl()
+ {
+ model = new DateTimeViewModel(true);
+ DataContext = model;
+ TimeTextBlock.DataContext = model;
+ DateTextBlock.DataContext = model;
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterKeyboardLayoutButton.xaml b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterKeyboardLayoutButton.xaml
new file mode 100644
index 00000000..58d50726
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterKeyboardLayoutButton.xaml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterKeyboardLayoutButton.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterKeyboardLayoutButton.xaml.cs
new file mode 100644
index 00000000..7777afbc
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterKeyboardLayoutButton.xaml.cs
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2019 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 System.Windows;
+using System.Windows.Controls;
+using SafeExamBrowser.Contracts.SystemComponents;
+using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Controls
+{
+ public partial class ActionCenterKeyboardLayoutButton : UserControl
+ {
+ private IKeyboardLayout layout;
+
+ public event KeyboardLayoutSelectedEventHandler LayoutSelected;
+
+ public string CultureCode
+ {
+ set { CultureCodeTextBlock.Text = value; }
+ }
+
+ public bool IsCurrent
+ {
+ set { IsCurrentTextBlock.Visibility = value ? Visibility.Visible : Visibility.Hidden; }
+ }
+
+ public string LayoutName
+ {
+ set { LayoutNameTextBlock.Text = value; }
+ }
+
+ public Guid LayoutId
+ {
+ get { return layout.Id; }
+ }
+
+ public ActionCenterKeyboardLayoutButton(IKeyboardLayout layout)
+ {
+ this.layout = layout;
+
+ InitializeComponent();
+ InitializeEvents();
+ }
+
+ private void InitializeEvents()
+ {
+ Button.Click += (o, args) => LayoutSelected?.Invoke(layout.Id);
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterKeyboardLayoutControl.xaml b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterKeyboardLayoutControl.xaml
new file mode 100644
index 00000000..d2afcb4a
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterKeyboardLayoutControl.xaml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterKeyboardLayoutControl.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterKeyboardLayoutControl.xaml.cs
new file mode 100644
index 00000000..1807cc17
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterKeyboardLayoutControl.xaml.cs
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2019 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 System.Threading.Tasks;
+using System.Windows.Controls;
+using System.Windows.Media;
+using SafeExamBrowser.Contracts.SystemComponents;
+using SafeExamBrowser.Contracts.UserInterface.Shell;
+using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Controls
+{
+ public partial class ActionCenterKeyboardLayoutControl : UserControl, ISystemKeyboardLayoutControl
+ {
+ public event KeyboardLayoutSelectedEventHandler LayoutSelected;
+
+ public ActionCenterKeyboardLayoutControl()
+ {
+ InitializeComponent();
+ InitializeKeyboardLayoutControl();
+ }
+
+ public void Add(IKeyboardLayout layout)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ var button = new ActionCenterKeyboardLayoutButton(layout);
+
+ button.LayoutSelected += Button_LayoutSelected;
+ button.CultureCode = layout.CultureCode;
+ button.LayoutName = layout.Name;
+
+ LayoutsStackPanel.Children.Add(button);
+ });
+ }
+
+ public void Close()
+ {
+ Dispatcher.Invoke(() => Popup.IsOpen = false);
+ }
+
+ public void SetCurrent(IKeyboardLayout layout)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ foreach (var child in LayoutsStackPanel.Children)
+ {
+ if (child is ActionCenterKeyboardLayoutButton layoutButton)
+ {
+ layoutButton.IsCurrent = layout.Id == layoutButton.LayoutId;
+ }
+ }
+
+ Text.Text = layout.Name;
+ });
+ }
+
+ public void SetInformation(string text)
+ {
+ Dispatcher.Invoke(() => Button.ToolTip = text);
+ }
+
+ private void InitializeKeyboardLayoutControl()
+ {
+ var originalBrush = Grid.Background;
+
+ Button.Click += (o, args) => Popup.IsOpen = !Popup.IsOpen;
+ 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;
+ Popup.Closed += (o, args) => Grid.Background = originalBrush;
+ }
+
+ private void Button_LayoutSelected(Guid id)
+ {
+ Popup.IsOpen = false;
+ LayoutSelected?.Invoke(id);
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterNotificationButton.xaml b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterNotificationButton.xaml
new file mode 100644
index 00000000..578d84a0
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterNotificationButton.xaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterNotificationButton.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterNotificationButton.xaml.cs
new file mode 100644
index 00000000..8f7ec048
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterNotificationButton.xaml.cs
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2019 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;
+using System.Windows.Controls;
+using SafeExamBrowser.Contracts.Client;
+using SafeExamBrowser.Contracts.UserInterface.Shell;
+using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
+using SafeExamBrowser.UserInterface.Mobile.Utilities;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Controls
+{
+ public partial class ActionCenterNotificationButton : UserControl, INotificationControl
+ {
+ public event NotificationControlClickedEventHandler Clicked;
+
+ public ActionCenterNotificationButton(INotificationInfo info)
+ {
+ InitializeComponent();
+ InitializeNotificationIcon(info);
+ }
+
+ private void Icon_Click(object sender, RoutedEventArgs e)
+ {
+ Clicked?.Invoke();
+ }
+
+ private void InitializeNotificationIcon(INotificationInfo info)
+ {
+ Icon.Content = IconResourceLoader.Load(info.IconResource);
+ IconButton.ToolTip = info.Tooltip;
+ Text.Text = info.Tooltip;
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterPowerSupplyControl.xaml b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterPowerSupplyControl.xaml
new file mode 100644
index 00000000..7c5c6725
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterPowerSupplyControl.xaml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterPowerSupplyControl.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterPowerSupplyControl.xaml.cs
new file mode 100644
index 00000000..5a6c269d
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterPowerSupplyControl.xaml.cs
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2019 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;
+using System.Windows.Controls;
+using System.Windows.Media;
+using SafeExamBrowser.Contracts.SystemComponents;
+using SafeExamBrowser.Contracts.UserInterface.Shell;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Controls
+{
+ public partial class ActionCenterPowerSupplyControl : UserControl, ISystemPowerSupplyControl
+ {
+ private double BATTERY_CHARGE_MAX_WIDTH;
+
+ public ActionCenterPowerSupplyControl()
+ {
+ InitializeComponent();
+ BATTERY_CHARGE_MAX_WIDTH = BatteryCharge.Width;
+ }
+
+ public void Close()
+ {
+ }
+
+ public void SetBatteryCharge(double charge, BatteryChargeStatus status)
+ {
+ Dispatcher.InvokeAsync(() =>
+ {
+ var width = BATTERY_CHARGE_MAX_WIDTH * charge;
+
+ width = width > BATTERY_CHARGE_MAX_WIDTH ? BATTERY_CHARGE_MAX_WIDTH : width;
+ width = width < 0 ? 0 : width;
+
+ BatteryCharge.Width = width;
+ BatteryCharge.Fill = status == BatteryChargeStatus.Low ? Brushes.Orange : BatteryCharge.Fill;
+ BatteryCharge.Fill = status == BatteryChargeStatus.Critical ? Brushes.Red : BatteryCharge.Fill;
+ Warning.Visibility = status == BatteryChargeStatus.Critical ? Visibility.Visible : Visibility.Collapsed;
+ });
+ }
+
+ public void SetPowerGridConnection(bool connected)
+ {
+ Dispatcher.InvokeAsync(() => PowerPlug.Visibility = connected ? Visibility.Visible : Visibility.Collapsed);
+ }
+
+ public void SetInformation(string text)
+ {
+ Dispatcher.InvokeAsync(() => Text.Text = text);
+ }
+
+ public void ShowCriticalBatteryWarning(string warning)
+ {
+ Dispatcher.InvokeAsync(() => Button.ToolTip = warning);
+ }
+
+ public void ShowLowBatteryInfo(string info)
+ {
+ Dispatcher.InvokeAsync(() => Button.ToolTip = info);
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterQuitButton.xaml b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterQuitButton.xaml
new file mode 100644
index 00000000..9d924722
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterQuitButton.xaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterQuitButton.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterQuitButton.xaml.cs
new file mode 100644
index 00000000..3ebcc509
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterQuitButton.xaml.cs
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2019 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 System.ComponentModel;
+using System.Windows.Controls;
+using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
+using SafeExamBrowser.UserInterface.Mobile.Utilities;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Controls
+{
+ public partial class ActionCenterQuitButton : UserControl
+ {
+ public event QuitButtonClickedEventHandler Clicked;
+
+ public ActionCenterQuitButton()
+ {
+ InitializeComponent();
+ InitializeControl();
+ }
+
+ private void InitializeControl()
+ {
+ var uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/ShutDown.xaml");
+ var resource = new XamlIconResource(uri);
+
+ Icon.Content = IconResourceLoader.Load(resource);
+ Button.Click += (o, args) => Clicked?.Invoke(new CancelEventArgs());
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterWirelessNetworkButton.xaml b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterWirelessNetworkButton.xaml
new file mode 100644
index 00000000..0d4451ea
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterWirelessNetworkButton.xaml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterWirelessNetworkButton.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterWirelessNetworkButton.xaml.cs
new file mode 100644
index 00000000..04929d32
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterWirelessNetworkButton.xaml.cs
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2019 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;
+using System.Windows.Controls;
+using SafeExamBrowser.Contracts.SystemComponents;
+using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Controls
+{
+ public partial class ActionCenterWirelessNetworkButton : UserControl
+ {
+ private IWirelessNetwork network;
+
+ public bool IsCurrent
+ {
+ set { IsCurrentTextBlock.Visibility = value ? Visibility.Visible : Visibility.Hidden; }
+ }
+
+ public string NetworkName
+ {
+ set { NetworkNameTextBlock.Text = value; }
+ }
+
+ public int SignalStrength
+ {
+ set { SignalStrengthTextBlock.Text = $"{value}%"; }
+ }
+
+ public event WirelessNetworkSelectedEventHandler NetworkSelected;
+
+ public ActionCenterWirelessNetworkButton(IWirelessNetwork network)
+ {
+ this.network = network;
+
+ InitializeComponent();
+ InitializeEvents();
+ }
+
+ private void InitializeEvents()
+ {
+ Button.Click += (o, args) => NetworkSelected?.Invoke(network.Id);
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterWirelessNetworkControl.xaml b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterWirelessNetworkControl.xaml
new file mode 100644
index 00000000..5bdf5973
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterWirelessNetworkControl.xaml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterWirelessNetworkControl.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterWirelessNetworkControl.xaml.cs
new file mode 100644
index 00000000..38b07766
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/ActionCenterWirelessNetworkControl.xaml.cs
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2019 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 System.Collections.Generic;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using FontAwesome.WPF;
+using SafeExamBrowser.Contracts.SystemComponents;
+using SafeExamBrowser.Contracts.UserInterface.Shell;
+using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
+using SafeExamBrowser.UserInterface.Mobile.Utilities;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Controls
+{
+ public partial class ActionCenterWirelessNetworkControl : UserControl, ISystemWirelessNetworkControl
+ {
+ public bool HasWirelessNetworkAdapter
+ {
+ set
+ {
+ Dispatcher.Invoke(() =>
+ {
+ Button.IsEnabled = value;
+ NoAdapterIcon.Visibility = value ? Visibility.Collapsed : Visibility.Visible;
+ });
+ }
+ }
+
+ public bool IsConnecting
+ {
+ set
+ {
+ Dispatcher.Invoke(() =>
+ {
+ LoadingIcon.Visibility = value ? Visibility.Visible : Visibility.Collapsed;
+ SignalStrengthIcon.Visibility = value ? Visibility.Collapsed : Visibility.Visible;
+ NetworkStatusIcon.Visibility = value ? Visibility.Collapsed : Visibility.Visible;
+ });
+ }
+ }
+
+ public WirelessNetworkStatus NetworkStatus
+ {
+ set
+ {
+ Dispatcher.Invoke(() =>
+ {
+ var icon = value == WirelessNetworkStatus.Connected ? FontAwesomeIcon.Check : FontAwesomeIcon.Close;
+ var brush = value == WirelessNetworkStatus.Connected ? Brushes.Green : Brushes.Orange;
+
+ if (value == WirelessNetworkStatus.Disconnected)
+ {
+ SignalStrengthIcon.Child = GetIcon(0);
+ }
+
+ NetworkStatusIcon.Source = ImageAwesome.CreateImageSource(icon, brush);
+ });
+ }
+ }
+
+ public event WirelessNetworkSelectedEventHandler NetworkSelected;
+
+ public ActionCenterWirelessNetworkControl()
+ {
+ InitializeComponent();
+ InitializeWirelessNetworkControl();
+ }
+
+ public void Close()
+ {
+ Dispatcher.Invoke(() => Popup.IsOpen = false);
+ }
+
+ public void SetInformation(string text)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ Button.ToolTip = text;
+ Text.Text = text;
+ });
+ }
+
+ public void Update(IEnumerable networks)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ NetworksStackPanel.Children.Clear();
+
+ foreach (var network in networks)
+ {
+ var button = new ActionCenterWirelessNetworkButton(network);
+ var isCurrent = network.Status == WirelessNetworkStatus.Connected;
+
+ button.IsCurrent = isCurrent;
+ button.NetworkName = network.Name;
+ button.SignalStrength = network.SignalStrength;
+ button.NetworkSelected += (id) => NetworkSelected?.Invoke(id);
+
+ if (isCurrent)
+ {
+ NetworkStatus = network.Status;
+ SignalStrengthIcon.Child = GetIcon(network.SignalStrength);
+ }
+
+ NetworksStackPanel.Children.Add(button);
+ }
+ });
+ }
+
+ private void InitializeWirelessNetworkControl()
+ {
+ var originalBrush = Grid.Background;
+
+ SignalStrengthIcon.Child = GetIcon(0);
+ Button.Click += (o, args) => Popup.IsOpen = !Popup.IsOpen;
+ 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;
+ Popup.Closed += (o, args) => Grid.Background = originalBrush;
+ }
+
+ private UIElement GetIcon(int signalStrength)
+ {
+ var icon = signalStrength > 66 ? "100" : (signalStrength > 33 ? "66" : (signalStrength > 0 ? "33" : "0"));
+ var uri = new Uri($"pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/WiFi_Light_{icon}.xaml");
+ var resource = new XamlIconResource(uri);
+
+ return IconResourceLoader.Load(resource);
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarApplicationControl.xaml b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarApplicationControl.xaml
new file mode 100644
index 00000000..c2eca719
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarApplicationControl.xaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarApplicationControl.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarApplicationControl.xaml.cs
new file mode 100644
index 00000000..2e59fe6e
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarApplicationControl.xaml.cs
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2019 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.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Threading;
+using SafeExamBrowser.Contracts.Applications;
+using SafeExamBrowser.Contracts.UserInterface.Shell;
+using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
+using SafeExamBrowser.UserInterface.Mobile.Utilities;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Controls
+{
+ public partial class TaskbarApplicationControl : UserControl, IApplicationControl
+ {
+ private IApplicationInfo info;
+ private IList instances = new List();
+
+ public event ApplicationControlClickedEventHandler Clicked;
+
+ public TaskbarApplicationControl(IApplicationInfo info)
+ {
+ this.info = info;
+
+ InitializeComponent();
+ InitializeApplicationControl();
+ }
+
+ public void RegisterInstance(IApplicationInstance instance)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ var instanceButton = new TaskbarApplicationInstanceButton(instance, info);
+
+ instanceButton.Clicked += (id) => Clicked?.Invoke(id);
+ instance.Terminated += (id) => Instance_OnTerminated(id, instanceButton);
+
+ instances.Add(instance);
+ InstanceStackPanel.Children.Add(instanceButton);
+ });
+ }
+
+ private void InitializeApplicationControl()
+ {
+ var originalBrush = Button.Background;
+
+ Button.ToolTip = info.Tooltip;
+ Button.Content = IconResourceLoader.Load(info.IconResource);
+
+ Button.MouseEnter += (o, args) => InstancePopup.IsOpen = instances.Count > 1;
+ Button.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => InstancePopup.IsOpen = InstancePopup.IsMouseOver));
+ InstancePopup.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => InstancePopup.IsOpen = IsMouseOver));
+
+ InstancePopup.Opened += (o, args) =>
+ {
+ Background = Brushes.LightGray;
+ Button.Background = Brushes.LightGray;
+ };
+
+ InstancePopup.Closed += (o, args) =>
+ {
+ Background = originalBrush;
+ Button.Background = originalBrush;
+ };
+ }
+
+ private void Button_Click(object sender, RoutedEventArgs e)
+ {
+ if (instances.Count <= 1)
+ {
+ Clicked?.Invoke(instances.FirstOrDefault()?.Id);
+ }
+ else
+ {
+ InstancePopup.IsOpen = true;
+ }
+ }
+
+ private void Instance_OnTerminated(InstanceIdentifier id, TaskbarApplicationInstanceButton instanceButton)
+ {
+ Dispatcher.InvokeAsync(() =>
+ {
+ instances.Remove(instances.FirstOrDefault(i => i.Id == id));
+ InstanceStackPanel.Children.Remove(instanceButton);
+ });
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarApplicationInstanceButton.xaml b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarApplicationInstanceButton.xaml
new file mode 100644
index 00000000..42ded0e0
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarApplicationInstanceButton.xaml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarApplicationInstanceButton.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarApplicationInstanceButton.xaml.cs
new file mode 100644
index 00000000..8aab1fee
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarApplicationInstanceButton.xaml.cs
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2019 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;
+using System.Windows.Controls;
+using SafeExamBrowser.Contracts.Applications;
+using SafeExamBrowser.Contracts.Core;
+using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
+using SafeExamBrowser.UserInterface.Mobile.Utilities;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Controls
+{
+ public partial class TaskbarApplicationInstanceButton : UserControl
+ {
+ private IApplicationInfo info;
+ private IApplicationInstance instance;
+
+ internal event ApplicationControlClickedEventHandler Clicked;
+
+ public TaskbarApplicationInstanceButton(IApplicationInstance instance, IApplicationInfo info)
+ {
+ this.info = info;
+ this.instance = instance;
+
+ InitializeComponent();
+ InitializeApplicationInstanceButton();
+ }
+
+ private void InitializeApplicationInstanceButton()
+ {
+ Icon.Content = IconResourceLoader.Load(info.IconResource);
+ Text.Text = instance.Name;
+ Button.ToolTip = instance.Name;
+
+ instance.IconChanged += Instance_IconChanged;
+ instance.NameChanged += Instance_NameChanged;
+ }
+
+ private void Instance_IconChanged(IIconResource icon)
+ {
+ Dispatcher.InvokeAsync(() => Icon.Content = IconResourceLoader.Load(icon));
+ }
+
+ private void Instance_NameChanged(string name)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ Text.Text = name;
+ Button.ToolTip = name;
+ });
+ }
+
+ private void Button_Click(object sender, RoutedEventArgs e)
+ {
+ Clicked?.Invoke(instance.Id);
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarClock.xaml b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarClock.xaml
new file mode 100644
index 00000000..8569e435
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarClock.xaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarClock.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarClock.xaml.cs
new file mode 100644
index 00000000..fca0aa72
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarClock.xaml.cs
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2019 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.Controls;
+using SafeExamBrowser.UserInterface.Mobile.ViewModels;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Controls
+{
+ public partial class TaskbarClock : UserControl
+ {
+ private DateTimeViewModel model;
+
+ public TaskbarClock()
+ {
+ InitializeComponent();
+ InitializeControl();
+ }
+
+ private void InitializeControl()
+ {
+ model = new DateTimeViewModel(false);
+ DataContext = model;
+ TimeTextBlock.DataContext = model;
+ DateTextBlock.DataContext = model;
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarKeyboardLayoutButton.xaml b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarKeyboardLayoutButton.xaml
new file mode 100644
index 00000000..c4092845
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarKeyboardLayoutButton.xaml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarKeyboardLayoutButton.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarKeyboardLayoutButton.xaml.cs
new file mode 100644
index 00000000..0d046da2
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarKeyboardLayoutButton.xaml.cs
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2019 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 System.Windows;
+using System.Windows.Controls;
+using SafeExamBrowser.Contracts.SystemComponents;
+using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Controls
+{
+ public partial class TaskbarKeyboardLayoutButton : UserControl
+ {
+ private IKeyboardLayout layout;
+
+ public event KeyboardLayoutSelectedEventHandler LayoutSelected;
+
+ public string CultureCode
+ {
+ set { CultureCodeTextBlock.Text = value; }
+ }
+
+ public bool IsCurrent
+ {
+ set { IsCurrentTextBlock.Visibility = value ? Visibility.Visible : Visibility.Hidden; }
+ }
+
+ public string LayoutName
+ {
+ set { LayoutNameTextBlock.Text = value; }
+ }
+
+ public Guid LayoutId
+ {
+ get { return layout.Id; }
+ }
+
+ public TaskbarKeyboardLayoutButton(IKeyboardLayout layout)
+ {
+ this.layout = layout;
+
+ InitializeComponent();
+ InitializeEvents();
+ }
+
+ private void InitializeEvents()
+ {
+ Button.Click += (o, args) => LayoutSelected?.Invoke(layout.Id);
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarKeyboardLayoutControl.xaml b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarKeyboardLayoutControl.xaml
new file mode 100644
index 00000000..7145d747
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarKeyboardLayoutControl.xaml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarKeyboardLayoutControl.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarKeyboardLayoutControl.xaml.cs
new file mode 100644
index 00000000..30aedb1c
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarKeyboardLayoutControl.xaml.cs
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2019 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 System.Linq;
+using System.Threading.Tasks;
+using System.Windows.Controls;
+using System.Windows.Media;
+using SafeExamBrowser.Contracts.SystemComponents;
+using SafeExamBrowser.Contracts.UserInterface.Shell;
+using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Controls
+{
+ public partial class TaskbarKeyboardLayoutControl : UserControl, ISystemKeyboardLayoutControl
+ {
+ public event KeyboardLayoutSelectedEventHandler LayoutSelected;
+
+ public TaskbarKeyboardLayoutControl()
+ {
+ InitializeComponent();
+ InitializeKeyboardLayoutControl();
+ }
+
+ public void Add(IKeyboardLayout layout)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ var button = new TaskbarKeyboardLayoutButton(layout);
+
+ button.LayoutSelected += Button_LayoutSelected;
+ button.CultureCode = layout.CultureCode;
+ button.LayoutName = layout.Name;
+
+ LayoutsStackPanel.Children.Add(button);
+ });
+ }
+
+ public void Close()
+ {
+ Dispatcher.Invoke(() => Popup.IsOpen = false);
+ }
+
+ public void SetCurrent(IKeyboardLayout layout)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ var name = layout.Name?.Length > 3 ? String.Join(string.Empty, layout.Name.Split(' ').Where(s => Char.IsLetter(s.First())).Select(s => s.First())) : layout.Name;
+
+ foreach (var child in LayoutsStackPanel.Children)
+ {
+ if (child is TaskbarKeyboardLayoutButton layoutButton)
+ {
+ layoutButton.IsCurrent = layout.Id == layoutButton.LayoutId;
+ }
+ }
+
+ LayoutCultureCode.Text = layout.CultureCode;
+ });
+ }
+
+ public void SetInformation(string text)
+ {
+ Dispatcher.Invoke(() => Button.ToolTip = text);
+ }
+
+ private void InitializeKeyboardLayoutControl()
+ {
+ var originalBrush = Button.Background;
+
+ Button.Click += (o, args) => Popup.IsOpen = !Popup.IsOpen;
+ 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) =>
+ {
+ Background = Brushes.LightGray;
+ Button.Background = Brushes.LightGray;
+ };
+
+ Popup.Closed += (o, args) =>
+ {
+ Background = originalBrush;
+ Button.Background = originalBrush;
+ };
+ }
+
+ private void Button_LayoutSelected(Guid id)
+ {
+ Popup.IsOpen = false;
+ LayoutSelected?.Invoke(id);
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarNotificationButton.xaml b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarNotificationButton.xaml
new file mode 100644
index 00000000..7486e8b3
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarNotificationButton.xaml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarNotificationButton.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarNotificationButton.xaml.cs
new file mode 100644
index 00000000..55e77802
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarNotificationButton.xaml.cs
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2019 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;
+using System.Windows.Controls;
+using SafeExamBrowser.Contracts.Client;
+using SafeExamBrowser.Contracts.UserInterface.Shell;
+using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
+using SafeExamBrowser.UserInterface.Mobile.Utilities;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Controls
+{
+ public partial class TaskbarNotificationButton : UserControl, INotificationControl
+ {
+ public event NotificationControlClickedEventHandler Clicked;
+
+ public TaskbarNotificationButton(INotificationInfo info)
+ {
+ InitializeComponent();
+ InitializeNotificationIcon(info);
+ }
+
+ private void Icon_Click(object sender, RoutedEventArgs e)
+ {
+ Clicked?.Invoke();
+ }
+
+ private void InitializeNotificationIcon(INotificationInfo info)
+ {
+ IconButton.ToolTip = info.Tooltip;
+ IconButton.Content = IconResourceLoader.Load(info.IconResource);
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarPowerSupplyControl.xaml b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarPowerSupplyControl.xaml
new file mode 100644
index 00000000..61b14e49
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarPowerSupplyControl.xaml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarPowerSupplyControl.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarPowerSupplyControl.xaml.cs
new file mode 100644
index 00000000..7a564e5f
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarPowerSupplyControl.xaml.cs
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2019 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;
+using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Threading;
+using SafeExamBrowser.Contracts.SystemComponents;
+using SafeExamBrowser.Contracts.UserInterface.Shell;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Controls
+{
+ public partial class TaskbarPowerSupplyControl : UserControl, ISystemPowerSupplyControl
+ {
+ private double BATTERY_CHARGE_MAX_WIDTH;
+
+ public TaskbarPowerSupplyControl()
+ {
+ InitializeComponent();
+ BATTERY_CHARGE_MAX_WIDTH = BatteryCharge.Width;
+ }
+
+ public void Close()
+ {
+ Popup.IsOpen = false;
+ }
+
+ public void SetBatteryCharge(double charge, BatteryChargeStatus status)
+ {
+ Dispatcher.InvokeAsync(() =>
+ {
+ var width = BATTERY_CHARGE_MAX_WIDTH * charge;
+
+ width = width > BATTERY_CHARGE_MAX_WIDTH ? BATTERY_CHARGE_MAX_WIDTH : width;
+ width = width < 0 ? 0 : width;
+
+ BatteryCharge.Width = width;
+ BatteryCharge.Fill = status == BatteryChargeStatus.Low ? Brushes.Orange : BatteryCharge.Fill;
+ BatteryCharge.Fill = status == BatteryChargeStatus.Critical ? Brushes.Red : BatteryCharge.Fill;
+ Warning.Visibility = status == BatteryChargeStatus.Critical ? Visibility.Visible : Visibility.Collapsed;
+ });
+ }
+
+ public void SetPowerGridConnection(bool connected)
+ {
+ Dispatcher.InvokeAsync(() => PowerPlug.Visibility = connected ? Visibility.Visible : Visibility.Collapsed);
+ }
+
+ public void SetInformation(string text)
+ {
+ Dispatcher.InvokeAsync(() => Button.ToolTip = text);
+ }
+
+ public void ShowCriticalBatteryWarning(string warning)
+ {
+ Dispatcher.InvokeAsync(() => ShowPopup(warning));
+ }
+
+ public void ShowLowBatteryInfo(string info)
+ {
+ Dispatcher.InvokeAsync(() => ShowPopup(info));
+ }
+
+ private void ShowPopup(string text)
+ {
+ Popup.IsOpen = true;
+ PopupText.Text = text;
+ Background = Brushes.LightGray;
+ }
+
+ private void Button_Click(object sender, RoutedEventArgs e)
+ {
+ Popup.IsOpen = false;
+ Background = Brushes.Transparent;
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarQuitButton.xaml b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarQuitButton.xaml
new file mode 100644
index 00000000..06d89d2e
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarQuitButton.xaml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarQuitButton.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarQuitButton.xaml.cs
new file mode 100644
index 00000000..79ef83f4
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarQuitButton.xaml.cs
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2019 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 System.ComponentModel;
+using System.Windows;
+using System.Windows.Controls;
+using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
+using SafeExamBrowser.UserInterface.Mobile.Utilities;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Controls
+{
+ public partial class TaskbarQuitButton : UserControl
+ {
+ public event QuitButtonClickedEventHandler Clicked;
+
+ public TaskbarQuitButton()
+ {
+ InitializeComponent();
+ LoadIcon();
+ }
+
+ private void Button_Click(object sender, RoutedEventArgs e)
+ {
+ Clicked?.Invoke(new CancelEventArgs());
+ }
+
+ private void LoadIcon()
+ {
+ var uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/ShutDown.xaml");
+ var resource = new XamlIconResource(uri);
+
+ Button.Content = IconResourceLoader.Load(resource);
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarWirelessNetworkButton.xaml b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarWirelessNetworkButton.xaml
new file mode 100644
index 00000000..5d80d452
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarWirelessNetworkButton.xaml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarWirelessNetworkButton.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarWirelessNetworkButton.xaml.cs
new file mode 100644
index 00000000..40d9da00
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarWirelessNetworkButton.xaml.cs
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2019 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;
+using System.Windows.Controls;
+using SafeExamBrowser.Contracts.SystemComponents;
+using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Controls
+{
+ public partial class TaskbarWirelessNetworkButton : UserControl
+ {
+ private readonly IWirelessNetwork network;
+
+ public bool IsCurrent
+ {
+ set { IsCurrentTextBlock.Visibility = value ? Visibility.Visible : Visibility.Hidden; }
+ }
+
+ public string NetworkName
+ {
+ set { NetworkNameTextBlock.Text = value; }
+ }
+
+ public int SignalStrength
+ {
+ set { SignalStrengthTextBlock.Text = $"{value}%"; }
+ }
+
+ public event WirelessNetworkSelectedEventHandler NetworkSelected;
+
+ public TaskbarWirelessNetworkButton(IWirelessNetwork network)
+ {
+ this.network = network;
+
+ InitializeComponent();
+ InitializeEvents();
+ }
+
+ private void InitializeEvents()
+ {
+ Button.Click += (o, args) => NetworkSelected?.Invoke(network.Id);
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarWirelessNetworkControl.xaml b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarWirelessNetworkControl.xaml
new file mode 100644
index 00000000..402b5750
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarWirelessNetworkControl.xaml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarWirelessNetworkControl.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarWirelessNetworkControl.xaml.cs
new file mode 100644
index 00000000..4042bac0
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Controls/TaskbarWirelessNetworkControl.xaml.cs
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2019 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 System.Collections.Generic;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using FontAwesome.WPF;
+using SafeExamBrowser.Contracts.SystemComponents;
+using SafeExamBrowser.Contracts.UserInterface.Shell;
+using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
+using SafeExamBrowser.UserInterface.Mobile.Utilities;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Controls
+{
+ public partial class TaskbarWirelessNetworkControl : UserControl, ISystemWirelessNetworkControl
+ {
+ public bool HasWirelessNetworkAdapter
+ {
+ set
+ {
+ Dispatcher.InvokeAsync(() =>
+ {
+ Button.IsEnabled = value;
+ NoAdapterIcon.Visibility = value ? Visibility.Collapsed : Visibility.Visible;
+ });
+ }
+ }
+
+ public bool IsConnecting
+ {
+ set
+ {
+ Dispatcher.Invoke(() =>
+ {
+ LoadingIcon.Visibility = value ? Visibility.Visible : Visibility.Collapsed;
+ SignalStrengthIcon.Visibility = value ? Visibility.Collapsed : Visibility.Visible;
+ NetworkStatusIcon.Visibility = value ? Visibility.Collapsed : Visibility.Visible;
+ });
+ }
+ }
+
+ public WirelessNetworkStatus NetworkStatus
+ {
+ set
+ {
+ Dispatcher.InvokeAsync(() =>
+ {
+ var icon = value == WirelessNetworkStatus.Connected ? FontAwesomeIcon.Check : FontAwesomeIcon.Close;
+ var brush = value == WirelessNetworkStatus.Connected ? Brushes.Green : Brushes.Orange;
+
+ if (value == WirelessNetworkStatus.Disconnected)
+ {
+ SignalStrengthIcon.Child = GetIcon(0);
+ }
+
+ NetworkStatusIcon.Source = ImageAwesome.CreateImageSource(icon, brush);
+ });
+ }
+ }
+
+ public event WirelessNetworkSelectedEventHandler NetworkSelected;
+
+ public TaskbarWirelessNetworkControl()
+ {
+ InitializeComponent();
+ InitializeWirelessNetworkControl();
+ }
+
+ public void Close()
+ {
+ Popup.IsOpen = false;
+ }
+
+ public void SetInformation(string text)
+ {
+ Dispatcher.InvokeAsync(() => Button.ToolTip = text);
+ }
+
+ public void Update(IEnumerable networks)
+ {
+ Dispatcher.InvokeAsync(() =>
+ {
+ NetworksStackPanel.Children.Clear();
+
+ foreach (var network in networks)
+ {
+ var button = new TaskbarWirelessNetworkButton(network);
+ var isCurrent = network.Status == WirelessNetworkStatus.Connected;
+
+ button.IsCurrent = isCurrent;
+ button.NetworkName = network.Name;
+ button.SignalStrength = network.SignalStrength;
+ button.NetworkSelected += (id) => NetworkSelected?.Invoke(id);
+
+ if (isCurrent)
+ {
+ NetworkStatus = network.Status;
+ SignalStrengthIcon.Child = GetIcon(network.SignalStrength);
+ }
+
+ NetworksStackPanel.Children.Add(button);
+ }
+ });
+ }
+
+ private void InitializeWirelessNetworkControl()
+ {
+ var originalBrush = Button.Background;
+
+ SignalStrengthIcon.Child = GetIcon(0);
+ Button.Click += (o, args) => Popup.IsOpen = !Popup.IsOpen;
+ 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) =>
+ {
+ Background = Brushes.LightGray;
+ Button.Background = Brushes.LightGray;
+ };
+
+ Popup.Closed += (o, args) =>
+ {
+ Background = originalBrush;
+ Button.Background = originalBrush;
+ };
+ }
+
+ private UIElement GetIcon(int signalStrength)
+ {
+ var icon = signalStrength > 66 ? "100" : (signalStrength > 33 ? "66" : (signalStrength > 0 ? "33" : "0"));
+ var uri = new Uri($"pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/WiFi_{icon}.xaml");
+ var resource = new XamlIconResource(uri);
+
+ return IconResourceLoader.Load(resource);
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Images/AboutNotification.xaml b/SafeExamBrowser.UserInterface.Mobile/Images/AboutNotification.xaml
new file mode 100644
index 00000000..7d70c974
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Images/AboutNotification.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Images/Battery.xaml b/SafeExamBrowser.UserInterface.Mobile/Images/Battery.xaml
new file mode 100644
index 00000000..998b72d4
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Images/Battery.xaml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Mobile/Images/Keyboard.xaml b/SafeExamBrowser.UserInterface.Mobile/Images/Keyboard.xaml
new file mode 100644
index 00000000..f90b0365
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Images/Keyboard.xaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Mobile/Images/LogNotification.ico b/SafeExamBrowser.UserInterface.Mobile/Images/LogNotification.ico
new file mode 100644
index 00000000..1c7fb20f
Binary files /dev/null and b/SafeExamBrowser.UserInterface.Mobile/Images/LogNotification.ico differ
diff --git a/SafeExamBrowser.UserInterface.Mobile/Images/Menu.xaml b/SafeExamBrowser.UserInterface.Mobile/Images/Menu.xaml
new file mode 100644
index 00000000..f9188146
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Images/Menu.xaml
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Images/NavigateBack.xaml b/SafeExamBrowser.UserInterface.Mobile/Images/NavigateBack.xaml
new file mode 100644
index 00000000..571e4427
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Images/NavigateBack.xaml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Mobile/Images/NavigateForward.xaml b/SafeExamBrowser.UserInterface.Mobile/Images/NavigateForward.xaml
new file mode 100644
index 00000000..957c1f83
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Images/NavigateForward.xaml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Mobile/Images/Reload.xaml b/SafeExamBrowser.UserInterface.Mobile/Images/Reload.xaml
new file mode 100644
index 00000000..eb620713
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Images/Reload.xaml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Mobile/Images/SafeExamBrowser.ico b/SafeExamBrowser.UserInterface.Mobile/Images/SafeExamBrowser.ico
new file mode 100644
index 00000000..abdc4635
Binary files /dev/null and b/SafeExamBrowser.UserInterface.Mobile/Images/SafeExamBrowser.ico differ
diff --git a/SafeExamBrowser.UserInterface.Mobile/Images/ShutDown.xaml b/SafeExamBrowser.UserInterface.Mobile/Images/ShutDown.xaml
new file mode 100644
index 00000000..a73e0262
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Images/ShutDown.xaml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Mobile/Images/SkipBack.xaml b/SafeExamBrowser.UserInterface.Mobile/Images/SkipBack.xaml
new file mode 100644
index 00000000..f7ff20bf
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Images/SkipBack.xaml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Mobile/Images/SplashScreen.png b/SafeExamBrowser.UserInterface.Mobile/Images/SplashScreen.png
new file mode 100644
index 00000000..c56dd2b0
Binary files /dev/null and b/SafeExamBrowser.UserInterface.Mobile/Images/SplashScreen.png differ
diff --git a/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_0.xaml b/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_0.xaml
new file mode 100644
index 00000000..ca328bff
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_0.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_100.xaml b/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_100.xaml
new file mode 100644
index 00000000..74fd45d8
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_100.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_33.xaml b/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_33.xaml
new file mode 100644
index 00000000..da1e2a15
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_33.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_66.xaml b/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_66.xaml
new file mode 100644
index 00000000..f6b214bc
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_66.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_Light_0.xaml b/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_Light_0.xaml
new file mode 100644
index 00000000..07d4c0f4
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_Light_0.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_Light_100.xaml b/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_Light_100.xaml
new file mode 100644
index 00000000..849e6b15
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_Light_100.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_Light_33.xaml b/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_Light_33.xaml
new file mode 100644
index 00000000..3c1820c4
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_Light_33.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_Light_66.xaml b/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_Light_66.xaml
new file mode 100644
index 00000000..006337de
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Images/WiFi_Light_66.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Mobile/Images/ZoomPageIn.xaml b/SafeExamBrowser.UserInterface.Mobile/Images/ZoomPageIn.xaml
new file mode 100644
index 00000000..e7f4acab
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Images/ZoomPageIn.xaml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Mobile/Images/ZoomPageOut.xaml b/SafeExamBrowser.UserInterface.Mobile/Images/ZoomPageOut.xaml
new file mode 100644
index 00000000..74a53e6c
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Images/ZoomPageOut.xaml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Mobile/LogWindow.xaml b/SafeExamBrowser.UserInterface.Mobile/LogWindow.xaml
new file mode 100644
index 00000000..fa88dc2a
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/LogWindow.xaml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/LogWindow.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/LogWindow.xaml.cs
new file mode 100644
index 00000000..9302f8fe
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/LogWindow.xaml.cs
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2019 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.Windows;
+using SafeExamBrowser.Contracts.I18n;
+using SafeExamBrowser.Contracts.Logging;
+using SafeExamBrowser.Contracts.UserInterface.Windows;
+using SafeExamBrowser.Contracts.UserInterface.Windows.Events;
+using SafeExamBrowser.UserInterface.Mobile.ViewModels;
+
+namespace SafeExamBrowser.UserInterface.Mobile
+{
+ public partial class LogWindow : Window, IWindow
+ {
+ private ILogger logger;
+ private LogViewModel model;
+ private WindowClosingEventHandler closing;
+
+ event WindowClosingEventHandler IWindow.Closing
+ {
+ add { closing += value; }
+ remove { closing -= value; }
+ }
+
+ public LogWindow(ILogger logger, IText text)
+ {
+ InitializeComponent();
+
+ this.logger = logger;
+ this.model = new LogViewModel(text, ScrollViewer, LogContent);
+
+ InitializeLogWindow();
+ }
+
+ public void BringToForeground()
+ {
+ Dispatcher.Invoke(() =>
+ {
+ if (WindowState == WindowState.Minimized)
+ {
+ WindowState = WindowState.Normal;
+ }
+
+ 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);
+ }
+
+ private void InitializeLogWindow()
+ {
+ DataContext = model;
+ Closing += LogWindow_Closing;
+ Loaded += LogWindow_Loaded;
+ }
+
+ private void LogWindow_Loaded(object sender, RoutedEventArgs e)
+ {
+ var log = logger.GetLog();
+
+ foreach (var content in log)
+ {
+ model.Notify(content);
+ }
+
+ logger.Subscribe(model);
+ logger.Debug("Opened log window.");
+ }
+
+ private void LogWindow_Closing(object sender, CancelEventArgs e)
+ {
+ logger.Unsubscribe(model);
+ logger.Debug("Closed log window.");
+
+ closing?.Invoke();
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/MessageBox.cs b/SafeExamBrowser.UserInterface.Mobile/MessageBox.cs
new file mode 100644
index 00000000..81aad442
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/MessageBox.cs
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2019 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;
+using SafeExamBrowser.Contracts.I18n;
+using SafeExamBrowser.Contracts.UserInterface.MessageBox;
+using SafeExamBrowser.Contracts.UserInterface.Windows;
+using MessageBoxResult = SafeExamBrowser.Contracts.UserInterface.MessageBox.MessageBoxResult;
+
+namespace SafeExamBrowser.UserInterface.Mobile
+{
+ public class MessageBox : IMessageBox
+ {
+ private IText text;
+
+ public MessageBox(IText text)
+ {
+ this.text = text;
+ }
+
+ public MessageBoxResult Show(string message, string title, MessageBoxAction action = MessageBoxAction.Confirm, MessageBoxIcon icon = MessageBoxIcon.Information, IWindow parent = null)
+ {
+ var result = default(System.Windows.MessageBoxResult);
+
+ if (parent is Window window)
+ {
+ result = window.Dispatcher.Invoke(() => System.Windows.MessageBox.Show(window, message, title, ToButton(action), ToImage(icon)));
+ }
+ else
+ {
+ result = System.Windows.MessageBox.Show(message, title, ToButton(action), ToImage(icon));
+ }
+
+ return ToResult(result);
+ }
+
+ public MessageBoxResult Show(TextKey message, TextKey title, MessageBoxAction action = MessageBoxAction.Confirm, MessageBoxIcon icon = MessageBoxIcon.Information, IWindow parent = null)
+ {
+ return Show(text.Get(message), text.Get(title), action, icon, parent);
+ }
+
+ private MessageBoxButton ToButton(MessageBoxAction action)
+ {
+ switch (action)
+ {
+ case MessageBoxAction.YesNo:
+ return MessageBoxButton.YesNo;
+ default:
+ return MessageBoxButton.OK;
+ }
+ }
+
+ private MessageBoxImage ToImage(MessageBoxIcon icon)
+ {
+ switch (icon)
+ {
+ case MessageBoxIcon.Error:
+ return MessageBoxImage.Error;
+ case MessageBoxIcon.Question:
+ return MessageBoxImage.Question;
+ case MessageBoxIcon.Warning:
+ return MessageBoxImage.Warning;
+ default:
+ return MessageBoxImage.Information;
+ }
+ }
+
+ private MessageBoxResult ToResult(System.Windows.MessageBoxResult result)
+ {
+ switch (result)
+ {
+ case System.Windows.MessageBoxResult.Cancel:
+ return MessageBoxResult.Cancel;
+ case System.Windows.MessageBoxResult.No:
+ return MessageBoxResult.No;
+ case System.Windows.MessageBoxResult.OK:
+ return MessageBoxResult.Ok;
+ case System.Windows.MessageBoxResult.Yes:
+ return MessageBoxResult.Yes;
+ default:
+ return MessageBoxResult.None;
+ }
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/PasswordDialog.xaml b/SafeExamBrowser.UserInterface.Mobile/PasswordDialog.xaml
new file mode 100644
index 00000000..45d7b52f
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/PasswordDialog.xaml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/PasswordDialog.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/PasswordDialog.xaml.cs
new file mode 100644
index 00000000..05e2bcb5
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/PasswordDialog.xaml.cs
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2019 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;
+using System.Windows.Input;
+using SafeExamBrowser.Contracts.I18n;
+using SafeExamBrowser.Contracts.UserInterface.Windows;
+using SafeExamBrowser.Contracts.UserInterface.Windows.Events;
+
+namespace SafeExamBrowser.UserInterface.Mobile
+{
+ public partial class PasswordDialog : Window, IPasswordDialog
+ {
+ private IText text;
+ private WindowClosingEventHandler closing;
+
+ event WindowClosingEventHandler IWindow.Closing
+ {
+ add { closing += value; }
+ remove { closing -= value; }
+ }
+
+ public PasswordDialog(string message, string title, IText text)
+ {
+ this.text = text;
+
+ InitializeComponent();
+ InitializePasswordDialog(message, title);
+ }
+
+ public void BringToForeground()
+ {
+ Dispatcher.Invoke(Activate);
+ }
+
+ public IPasswordDialogResult Show(IWindow parent = null)
+ {
+ return Dispatcher.Invoke(() =>
+ {
+ var result = new PasswordDialogResult { Success = false };
+
+ if (parent is Window)
+ {
+ Owner = parent as Window;
+ WindowStartupLocation = WindowStartupLocation.CenterOwner;
+ }
+
+ if (ShowDialog() is true)
+ {
+ result.Password = Password.Password;
+ result.Success = true;
+ }
+
+ return result;
+ });
+ }
+
+ private void InitializePasswordDialog(string message, string title)
+ {
+ Message.Text = message;
+ Title = title;
+ WindowStartupLocation = WindowStartupLocation.CenterScreen;
+
+ CancelButton.Content = text.Get(TextKey.PasswordDialog_Cancel);
+ CancelButton.Click += CancelButton_Click;
+
+ ConfirmButton.Content = text.Get(TextKey.PasswordDialog_Confirm);
+ ConfirmButton.Click += ConfirmButton_Click;
+
+ Closing += (o, args) => closing?.Invoke();
+ Password.KeyUp += Password_KeyUp;
+ }
+
+ private void CancelButton_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ Close();
+ }
+
+ private void ConfirmButton_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = true;
+ Close();
+ }
+
+ private void Password_KeyUp(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Enter)
+ {
+ DialogResult = true;
+ Close();
+ }
+ }
+
+ private class PasswordDialogResult : IPasswordDialogResult
+ {
+ public string Password { get; set; }
+ public bool Success { get; set; }
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Properties/AssemblyInfo.cs b/SafeExamBrowser.UserInterface.Mobile/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..9944deca
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Properties/AssemblyInfo.cs
@@ -0,0 +1,55 @@
+using System.Reflection;
+using System.Resources;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Windows;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("SafeExamBrowser.UserInterface.Mobile")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("SafeExamBrowser.UserInterface.Mobile")]
+[assembly: AssemblyCopyright("Copyright © 2019")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+//In order to begin building localizable applications, set
+//CultureYouAreCodingWith in your .csproj file
+//inside a . For example, if you are using US english
+//in your source files, set the to en-US. Then uncomment
+//the NeutralResourceLanguage attribute below. Update the "en-US" in
+//the line below to match the UICulture setting in the project file.
+
+//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
+
+
+[assembly:ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
+
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/SafeExamBrowser.UserInterface.Mobile/RuntimeWindow.xaml b/SafeExamBrowser.UserInterface.Mobile/RuntimeWindow.xaml
new file mode 100644
index 00000000..f7ebd6bb
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/RuntimeWindow.xaml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/RuntimeWindow.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/RuntimeWindow.xaml.cs
new file mode 100644
index 00000000..319e6174
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/RuntimeWindow.xaml.cs
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2019 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;
+using System.Windows.Documents;
+using SafeExamBrowser.Contracts.Configuration;
+using SafeExamBrowser.Contracts.I18n;
+using SafeExamBrowser.Contracts.Logging;
+using SafeExamBrowser.Contracts.UserInterface.Windows;
+using SafeExamBrowser.Contracts.UserInterface.Windows.Events;
+using SafeExamBrowser.UserInterface.Mobile.ViewModels;
+
+namespace SafeExamBrowser.UserInterface.Mobile
+{
+ public partial class RuntimeWindow : Window, IRuntimeWindow
+ {
+ private bool allowClose;
+ private AppConfig appConfig;
+ private IText text;
+ private RuntimeWindowViewModel model;
+ private WindowClosingEventHandler closing;
+
+ public bool TopMost
+ {
+ get { return Dispatcher.Invoke(() => Topmost); }
+ set { Dispatcher.Invoke(() => Topmost = value); }
+ }
+
+ event WindowClosingEventHandler IWindow.Closing
+ {
+ add { closing += value; }
+ remove { closing -= value; }
+ }
+
+ public RuntimeWindow(AppConfig appConfig, IText text)
+ {
+ this.appConfig = appConfig;
+ this.text = text;
+
+ InitializeComponent();
+ InitializeRuntimeWindow();
+ }
+
+ public void BringToForeground()
+ {
+ Dispatcher.Invoke(Activate);
+ }
+
+ public new void Close()
+ {
+ Dispatcher.Invoke(() =>
+ {
+ allowClose = true;
+ model.BusyIndication = false;
+
+ base.Close();
+ });
+ }
+
+ public new void Hide()
+ {
+ Dispatcher.Invoke(base.Hide);
+ }
+
+ public void HideProgressBar()
+ {
+ model.AnimatedBorderVisibility = Visibility.Visible;
+ model.ProgressBarVisibility = Visibility.Hidden;
+ }
+
+ public void Notify(ILogContent content)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ model.Notify(content);
+ LogScrollViewer.ScrollToEnd();
+ });
+ }
+
+ public void Progress()
+ {
+ model.CurrentProgress += 1;
+ }
+
+ public void Regress()
+ {
+ model.CurrentProgress -= 1;
+ }
+
+ public void SetIndeterminate()
+ {
+ model.IsIndeterminate = true;
+ }
+
+ public void SetMaxValue(int max)
+ {
+ model.MaxProgress = max;
+ }
+
+ public void SetValue(int value)
+ {
+ model.CurrentProgress = value;
+ }
+
+ public void ShowProgressBar()
+ {
+ model.AnimatedBorderVisibility = Visibility.Hidden;
+ model.ProgressBarVisibility = Visibility.Visible;
+ }
+
+ public void UpdateStatus(TextKey key, bool busyIndication = false)
+ {
+ model.Status = text.Get(key);
+ model.BusyIndication = busyIndication;
+ }
+
+ public new void Show()
+ {
+ Dispatcher.Invoke(base.Show);
+ }
+
+ private void InitializeRuntimeWindow()
+ {
+ Title = $"{appConfig.ProgramTitle} - Version {appConfig.ProgramVersion}";
+
+ InfoTextBlock.Inlines.Add(new Run($"Version {appConfig.ProgramVersion}") { FontStyle = FontStyles.Italic });
+ InfoTextBlock.Inlines.Add(new LineBreak());
+ InfoTextBlock.Inlines.Add(new LineBreak());
+ InfoTextBlock.Inlines.Add(new Run(appConfig.ProgramCopyright) { FontSize = 10 });
+
+ model = new RuntimeWindowViewModel(LogTextBlock);
+ AnimatedBorder.DataContext = model;
+ ProgressBar.DataContext = model;
+ StatusTextBlock.DataContext = model;
+
+ Closing += (o, args) => args.Cancel = !allowClose;
+
+#if DEBUG
+ Topmost = false;
+#endif
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/SafeExamBrowser.UserInterface.Mobile.csproj b/SafeExamBrowser.UserInterface.Mobile/SafeExamBrowser.UserInterface.Mobile.csproj
new file mode 100644
index 00000000..ff24fac7
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/SafeExamBrowser.UserInterface.Mobile.csproj
@@ -0,0 +1,387 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {89BC24DD-FF31-496E-9816-A160B686A3D4}
+ library
+ SafeExamBrowser.UserInterface.Mobile
+ SafeExamBrowser.UserInterface.Mobile
+ v4.7.2
+ 512
+ {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ 4
+ true
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+ true
+ bin\x86\Debug\
+ DEBUG;TRACE
+ full
+ x86
+ prompt
+ MinimumRecommendedRules.ruleset
+
+
+ bin\x86\Release\
+ TRACE
+ true
+ pdbonly
+ x86
+ prompt
+ MinimumRecommendedRules.ruleset
+
+
+
+ ..\packages\FontAwesome.WPF.4.7.0.9\lib\net40\FontAwesome.WPF.dll
+
+
+
+
+
+
+ 4.0
+
+
+
+
+
+
+
+
+ AboutWindow.xaml
+
+
+ ActionCenter.xaml
+
+
+ BrowserWindow.xaml
+
+
+ ActionCenterApplicationButton.xaml
+
+
+ ActionCenterApplicationControl.xaml
+
+
+ ActionCenterClock.xaml
+
+
+ ActionCenterKeyboardLayoutButton.xaml
+
+
+ ActionCenterKeyboardLayoutControl.xaml
+
+
+ ActionCenterNotificationButton.xaml
+
+
+ ActionCenterPowerSupplyControl.xaml
+
+
+ ActionCenterQuitButton.xaml
+
+
+ ActionCenterWirelessNetworkButton.xaml
+
+
+ ActionCenterWirelessNetworkControl.xaml
+
+
+ TaskbarApplicationControl.xaml
+
+
+ TaskbarApplicationInstanceButton.xaml
+
+
+ TaskbarClock.xaml
+
+
+ TaskbarKeyboardLayoutButton.xaml
+
+
+ TaskbarKeyboardLayoutControl.xaml
+
+
+ TaskbarNotificationButton.xaml
+
+
+ TaskbarPowerSupplyControl.xaml
+
+
+ TaskbarQuitButton.xaml
+
+
+ TaskbarWirelessNetworkButton.xaml
+
+
+ TaskbarWirelessNetworkControl.xaml
+
+
+ LogWindow.xaml
+
+
+
+ PasswordDialog.xaml
+
+
+ Code
+
+
+ RuntimeWindow.xaml
+
+
+ SplashScreen.xaml
+
+
+ Taskbar.xaml
+
+
+
+
+
+
+
+
+
+
+
+
+ {47da5933-bef8-4729-94e6-abde2db12262}
+ SafeExamBrowser.Contracts
+
+
+
+
+ MSBuild:Compile
+ Designer
+
+
+ Designer
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+ Designer
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Mobile/SplashScreen.xaml b/SafeExamBrowser.UserInterface.Mobile/SplashScreen.xaml
new file mode 100644
index 00000000..7b9fbaef
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/SplashScreen.xaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/SplashScreen.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/SplashScreen.xaml.cs
new file mode 100644
index 00000000..3a270cc5
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/SplashScreen.xaml.cs
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2019 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;
+using System.Windows.Documents;
+using SafeExamBrowser.Contracts.Configuration;
+using SafeExamBrowser.Contracts.I18n;
+using SafeExamBrowser.Contracts.UserInterface.Windows;
+using SafeExamBrowser.Contracts.UserInterface.Windows.Events;
+using SafeExamBrowser.UserInterface.Mobile.ViewModels;
+
+namespace SafeExamBrowser.UserInterface.Mobile
+{
+ public partial class SplashScreen : Window, ISplashScreen
+ {
+ private bool allowClose;
+ private ProgressIndicatorViewModel model = new ProgressIndicatorViewModel();
+ private AppConfig appConfig;
+ private IText text;
+ private WindowClosingEventHandler closing;
+
+ public AppConfig AppConfig
+ {
+ set
+ {
+ Dispatcher.Invoke(() =>
+ {
+ appConfig = value;
+ UpdateAppInfo();
+ });
+ }
+ }
+
+ event WindowClosingEventHandler IWindow.Closing
+ {
+ add { closing += value; }
+ remove { closing -= value; }
+ }
+
+ public SplashScreen(IText text, AppConfig appConfig = null)
+ {
+ this.appConfig = appConfig;
+ this.text = text;
+
+ InitializeComponent();
+ InitializeSplashScreen();
+ }
+
+ public void BringToForeground()
+ {
+ Dispatcher.Invoke(Activate);
+ }
+
+ public new void Close()
+ {
+ Dispatcher.Invoke(() =>
+ {
+ allowClose = true;
+ model.BusyIndication = false;
+
+ base.Close();
+ });
+ }
+
+ public new void Hide()
+ {
+ Dispatcher.Invoke(base.Hide);
+ }
+
+ public new void Show()
+ {
+ Dispatcher.Invoke(base.Show);
+ }
+
+ public void Progress()
+ {
+ model.CurrentProgress += 1;
+ }
+
+ public void Regress()
+ {
+ model.CurrentProgress -= 1;
+ }
+
+ public void SetIndeterminate()
+ {
+ model.IsIndeterminate = true;
+ }
+
+ public void SetMaxValue(int max)
+ {
+ model.MaxProgress = max;
+ }
+
+ public void SetValue(int value)
+ {
+ model.CurrentProgress = value;
+ }
+
+ public void UpdateStatus(TextKey key, bool busyIndication = false)
+ {
+ model.Status = text.Get(key);
+ model.BusyIndication = busyIndication;
+ }
+
+ private void InitializeSplashScreen()
+ {
+ UpdateAppInfo();
+
+ StatusTextBlock.DataContext = model;
+ ProgressBar.DataContext = model;
+
+ Closing += (o, args) => args.Cancel = !allowClose;
+ }
+
+ private void UpdateAppInfo()
+ {
+ if (appConfig != null)
+ {
+ InfoTextBlock.Inlines.Add(new Run($"Version {appConfig.ProgramVersion}") { FontStyle = FontStyles.Italic });
+ InfoTextBlock.Inlines.Add(new LineBreak());
+ InfoTextBlock.Inlines.Add(new LineBreak());
+ InfoTextBlock.Inlines.Add(new Run(appConfig.ProgramCopyright) { FontSize = 10 });
+ }
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Taskbar.xaml b/SafeExamBrowser.UserInterface.Mobile/Taskbar.xaml
new file mode 100644
index 00000000..e86741c6
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Taskbar.xaml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SafeExamBrowser.UserInterface.Mobile/Taskbar.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Taskbar.xaml.cs
new file mode 100644
index 00000000..c0cc5145
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Taskbar.xaml.cs
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2019 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.Windows;
+using SafeExamBrowser.Contracts.I18n;
+using SafeExamBrowser.Contracts.Logging;
+using SafeExamBrowser.Contracts.UserInterface.Shell;
+using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
+using SafeExamBrowser.UserInterface.Mobile.Utilities;
+
+namespace SafeExamBrowser.UserInterface.Mobile
+{
+ public partial class Taskbar : Window, ITaskbar
+ {
+ private bool allowClose;
+ private ILogger logger;
+
+ public bool ShowClock
+ {
+ set { Dispatcher.Invoke(() => Clock.Visibility = value ? Visibility.Visible : Visibility.Collapsed); }
+ }
+
+ public event QuitButtonClickedEventHandler QuitButtonClicked;
+
+ public Taskbar(ILogger logger)
+ {
+ this.logger = logger;
+
+ InitializeComponent();
+ InitializeTaskbar();
+ }
+
+ public void AddApplicationControl(IApplicationControl control)
+ {
+ if (control is UIElement uiElement)
+ {
+ ApplicationStackPanel.Children.Add(uiElement);
+ }
+ }
+
+ public void AddNotificationControl(INotificationControl control)
+ {
+ if (control is UIElement uiElement)
+ {
+ NotificationStackPanel.Children.Add(uiElement);
+ }
+ }
+
+ public void AddSystemControl(ISystemControl control)
+ {
+ if (control is UIElement uiElement)
+ {
+ SystemControlStackPanel.Children.Add(uiElement);
+ }
+ }
+
+ public new void Close()
+ {
+ Dispatcher.Invoke(base.Close);
+ }
+
+ public int GetAbsoluteHeight()
+ {
+ return Dispatcher.Invoke(() =>
+ {
+ var height = (int) this.TransformToPhysical(Width, Height).Y;
+
+ logger.Debug($"Calculated physical taskbar height is {height}px.");
+
+ return height;
+ });
+ }
+
+ public int GetRelativeHeight()
+ {
+ return Dispatcher.Invoke(() =>
+ {
+ var height = (int) Height;
+
+ logger.Debug($"Logical taskbar height is {height}px.");
+
+ return height;
+ });
+ }
+
+ public void InitializeBounds()
+ {
+ Dispatcher.Invoke(() =>
+ {
+ Width = SystemParameters.PrimaryScreenWidth;
+ Left = 0;
+ Top = SystemParameters.PrimaryScreenHeight - Height;
+
+ var position = this.TransformToPhysical(Left, Top);
+ var size = this.TransformToPhysical(Width, Height);
+
+ logger.Debug($"Set taskbar bounds to {Width}x{Height} at ({Left}/{Top}), in physical pixels: {size.X}x{size.Y} at ({position.X}/{position.Y}).");
+ });
+ }
+
+ public void InitializeText(IText text)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ QuitButton.ToolTip = text.Get(TextKey.Shell_QuitButton);
+ });
+ }
+
+ public new void Show()
+ {
+ Dispatcher.Invoke(base.Show);
+ }
+
+ private void QuitButton_Clicked(CancelEventArgs args)
+ {
+ QuitButtonClicked?.Invoke(args);
+ allowClose = !args.Cancel;
+ }
+
+ private void Taskbar_Closing(object sender, CancelEventArgs e)
+ {
+ if (!allowClose)
+ {
+ e.Cancel = true;
+
+ return;
+ }
+
+ foreach (var child in SystemControlStackPanel.Children)
+ {
+ if (child is ISystemControl systemControl)
+ {
+ systemControl.Close();
+ }
+ }
+ }
+
+ private void InitializeTaskbar()
+ {
+ Closing += Taskbar_Closing;
+ Loaded += (o, args) => InitializeBounds();
+ QuitButton.Clicked += QuitButton_Clicked;
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Templates/Buttons.xaml b/SafeExamBrowser.UserInterface.Mobile/Templates/Buttons.xaml
new file mode 100644
index 00000000..823e3cd2
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Templates/Buttons.xaml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Mobile/Templates/Colors.xaml b/SafeExamBrowser.UserInterface.Mobile/Templates/Colors.xaml
new file mode 100644
index 00000000..468c0504
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Templates/Colors.xaml
@@ -0,0 +1,5 @@
+
+ #FFF0F0F0
+ #AA808080
+
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Mobile/Templates/ScrollViewers.xaml b/SafeExamBrowser.UserInterface.Mobile/Templates/ScrollViewers.xaml
new file mode 100644
index 00000000..62595500
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Templates/ScrollViewers.xaml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.UserInterface.Mobile/UserInterfaceFactory.cs b/SafeExamBrowser.UserInterface.Mobile/UserInterfaceFactory.cs
new file mode 100644
index 00000000..424868e7
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/UserInterfaceFactory.cs
@@ -0,0 +1,179 @@
+/*
+ * Copyright (c) 2019 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.Threading;
+using System.Windows;
+using System.Windows.Media;
+using FontAwesome.WPF;
+using SafeExamBrowser.Contracts.Applications;
+using SafeExamBrowser.Contracts.Client;
+using SafeExamBrowser.Contracts.Configuration;
+using SafeExamBrowser.Contracts.Configuration.Settings;
+using SafeExamBrowser.Contracts.I18n;
+using SafeExamBrowser.Contracts.Logging;
+using SafeExamBrowser.Contracts.UserInterface;
+using SafeExamBrowser.Contracts.UserInterface.Browser;
+using SafeExamBrowser.Contracts.UserInterface.Shell;
+using SafeExamBrowser.Contracts.UserInterface.Windows;
+using SafeExamBrowser.UserInterface.Mobile.Controls;
+
+namespace SafeExamBrowser.UserInterface.Mobile
+{
+ public class UserInterfaceFactory : IUserInterfaceFactory
+ {
+ private IText text;
+
+ public UserInterfaceFactory(IText text)
+ {
+ this.text = text;
+
+ InitializeFontAwesome();
+ }
+
+ public IWindow CreateAboutWindow(AppConfig appConfig)
+ {
+ return new AboutWindow(appConfig, text);
+ }
+
+ public IApplicationControl CreateApplicationControl(IApplicationInfo info, Location location)
+ {
+ if (location == Location.ActionCenter)
+ {
+ return new ActionCenterApplicationControl(info);
+ }
+ else
+ {
+ return new TaskbarApplicationControl(info);
+ }
+ }
+
+ public IBrowserWindow CreateBrowserWindow(IBrowserControl control, BrowserSettings settings, bool isMainWindow)
+ {
+ return new BrowserWindow(control, settings, isMainWindow, text);
+ }
+
+ public ISystemKeyboardLayoutControl CreateKeyboardLayoutControl(Location location)
+ {
+ if (location == Location.ActionCenter)
+ {
+ return new ActionCenterKeyboardLayoutControl();
+ }
+ else
+ {
+ return new TaskbarKeyboardLayoutControl();
+ }
+ }
+
+ public IWindow CreateLogWindow(ILogger logger)
+ {
+ LogWindow logWindow = null;
+ var logWindowReadyEvent = new AutoResetEvent(false);
+ var logWindowThread = new Thread(() =>
+ {
+ logWindow = new LogWindow(logger, text);
+ logWindow.Closed += (o, args) => logWindow.Dispatcher.InvokeShutdown();
+ logWindow.Show();
+
+ logWindowReadyEvent.Set();
+
+ System.Windows.Threading.Dispatcher.Run();
+ });
+
+ logWindowThread.SetApartmentState(ApartmentState.STA);
+ logWindowThread.IsBackground = true;
+ logWindowThread.Start();
+
+ logWindowReadyEvent.WaitOne();
+
+ return logWindow;
+ }
+
+ public INotificationControl CreateNotificationControl(INotificationInfo info, Location location)
+ {
+ if (location == Location.ActionCenter)
+ {
+ return new ActionCenterNotificationButton(info);
+ }
+ else
+ {
+ return new TaskbarNotificationButton(info);
+ }
+ }
+
+ public IPasswordDialog CreatePasswordDialog(string message, string title)
+ {
+ return Application.Current.Dispatcher.Invoke(() => new PasswordDialog(message, title, text));
+ }
+
+ public IPasswordDialog CreatePasswordDialog(TextKey message, TextKey title)
+ {
+ return Application.Current.Dispatcher.Invoke(() => new PasswordDialog(text.Get(message), text.Get(title), text));
+ }
+
+ public ISystemPowerSupplyControl CreatePowerSupplyControl(Location location)
+ {
+ if (location == Location.ActionCenter)
+ {
+ return new ActionCenterPowerSupplyControl();
+ }
+ else
+ {
+ return new TaskbarPowerSupplyControl();
+ }
+ }
+
+ public IRuntimeWindow CreateRuntimeWindow(AppConfig appConfig)
+ {
+ return Application.Current.Dispatcher.Invoke(() => new RuntimeWindow(appConfig, text));
+ }
+
+ public ISplashScreen CreateSplashScreen(AppConfig appConfig = null)
+ {
+ SplashScreen splashScreen = null;
+ var splashReadyEvent = new AutoResetEvent(false);
+ var splashScreenThread = new Thread(() =>
+ {
+ splashScreen = new SplashScreen(text, appConfig);
+ splashScreen.Closed += (o, args) => splashScreen.Dispatcher.InvokeShutdown();
+ splashScreen.Show();
+
+ splashReadyEvent.Set();
+
+ System.Windows.Threading.Dispatcher.Run();
+ });
+
+ splashScreenThread.SetApartmentState(ApartmentState.STA);
+ splashScreenThread.Name = nameof(SplashScreen);
+ splashScreenThread.IsBackground = true;
+ splashScreenThread.Start();
+
+ splashReadyEvent.WaitOne();
+
+ return splashScreen;
+ }
+
+ public ISystemWirelessNetworkControl CreateWirelessNetworkControl(Location location)
+ {
+ if (location == Location.ActionCenter)
+ {
+ return new ActionCenterWirelessNetworkControl();
+ }
+ else
+ {
+ return new TaskbarWirelessNetworkControl();
+ }
+ }
+
+ private void InitializeFontAwesome()
+ {
+ // To be able to use FontAwesome in XAML icon resources, we need to make sure that the FontAwesome.WPF assembly is loaded into
+ // the AppDomain before attempting to load an icon resource - thus the creation of an unused image below...
+ ImageAwesome.CreateImageSource(FontAwesomeIcon.FontAwesome, Brushes.Black);
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Utilities/IconResourceLoader.cs b/SafeExamBrowser.UserInterface.Mobile/Utilities/IconResourceLoader.cs
new file mode 100644
index 00000000..54671505
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Utilities/IconResourceLoader.cs
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2019 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 System.Windows;
+using System.Windows.Controls;
+using System.Windows.Documents;
+using System.Windows.Markup;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using SafeExamBrowser.Contracts.Core;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Utilities
+{
+ ///
+ /// TODO: Move to shared library?
+ ///
+ internal static class IconResourceLoader
+ {
+ internal static UIElement Load(IIconResource resource)
+ {
+ try
+ {
+ if (resource.IsBitmapResource)
+ {
+ return LoadBitmapResource(resource);
+ }
+ else if (resource.IsXamlResource)
+ {
+ return LoadXamlResource(resource);
+ }
+ }
+ catch (Exception)
+ {
+ return NotFoundSymbol();
+ }
+
+ throw new NotSupportedException($"Application icon resource of type '{resource.GetType()}' is not supported!");
+ }
+
+ private static UIElement LoadBitmapResource(IIconResource resource)
+ {
+ return new Image
+ {
+ Source = new BitmapImage(resource.Uri)
+ };
+ }
+
+ private static UIElement LoadXamlResource(IIconResource resource)
+ {
+ using (var stream = Application.GetResourceStream(resource.Uri)?.Stream)
+ {
+ return XamlReader.Load(stream) as UIElement;
+ }
+ }
+
+ private static UIElement NotFoundSymbol()
+ {
+ return new TextBlock(new Run("X") { Foreground = Brushes.Red, FontWeight = FontWeights.Bold })
+ {
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center
+ };
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Utilities/VisualExtensions.cs b/SafeExamBrowser.UserInterface.Mobile/Utilities/VisualExtensions.cs
new file mode 100644
index 00000000..4a6a7d92
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Utilities/VisualExtensions.cs
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2019 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;
+using System.Windows.Interop;
+using System.Windows.Media;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Utilities
+{
+ ///
+ /// TODO: Move to shared library?
+ ///
+ internal static class VisualExtensions
+ {
+ ///
+ /// WPF works with device-independent pixels. This method is required to
+ /// transform such values to their absolute, device-specific pixel value.
+ /// Source: https://stackoverflow.com/questions/3286175/how-do-i-convert-a-wpf-size-to-physical-pixels
+ ///
+ internal static Vector TransformToPhysical(this Visual visual, double x, double y)
+ {
+ Matrix transformToDevice;
+ var source = PresentationSource.FromVisual(visual);
+
+ if (source != null)
+ {
+ transformToDevice = source.CompositionTarget.TransformToDevice;
+ }
+ else
+ {
+ using (var newSource = new HwndSource(new HwndSourceParameters()))
+ {
+ transformToDevice = newSource.CompositionTarget.TransformToDevice;
+ }
+ }
+
+ return transformToDevice.Transform(new Vector(x, y));
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/Utilities/XamlIconResource.cs b/SafeExamBrowser.UserInterface.Mobile/Utilities/XamlIconResource.cs
new file mode 100644
index 00000000..5e74c5a2
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/Utilities/XamlIconResource.cs
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2019 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.Core;
+
+namespace SafeExamBrowser.UserInterface.Mobile.Utilities
+{
+ ///
+ /// TODO: Move to shared library?
+ ///
+ internal class XamlIconResource : IIconResource
+ {
+ public Uri Uri { get; private set; }
+ public bool IsBitmapResource => false;
+ public bool IsXamlResource => true;
+
+ public XamlIconResource(Uri uri)
+ {
+ Uri = uri;
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/ViewModels/DateTimeViewModel.cs b/SafeExamBrowser.UserInterface.Mobile/ViewModels/DateTimeViewModel.cs
new file mode 100644
index 00000000..5f089aa1
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/ViewModels/DateTimeViewModel.cs
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2019 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 System.ComponentModel;
+using System.Timers;
+
+namespace SafeExamBrowser.UserInterface.Mobile.ViewModels
+{
+ internal class DateTimeViewModel : INotifyPropertyChanged
+ {
+ private Timer timer;
+ private readonly bool showSeconds;
+
+ public string Date { get; private set; }
+ public string Time { get; private set; }
+ public string ToolTip { get; private set; }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public DateTimeViewModel(bool showSeconds)
+ {
+ this.showSeconds = showSeconds;
+ this.timer = new Timer(1000);
+ this.timer.Elapsed += Timer_Elapsed;
+ this.timer.Start();
+ }
+
+ private void Timer_Elapsed(object sender, ElapsedEventArgs e)
+ {
+ var date = DateTime.Now;
+
+ Date = date.ToShortDateString();
+ Time = showSeconds ? date.ToLongTimeString() : date.ToShortTimeString();
+ ToolTip = $"{date.ToLongDateString()} {date.ToLongTimeString()}";
+
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Time)));
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Date)));
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ToolTip)));
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/ViewModels/LogViewModel.cs b/SafeExamBrowser.UserInterface.Mobile/ViewModels/LogViewModel.cs
new file mode 100644
index 00000000..b09e3dc5
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/ViewModels/LogViewModel.cs
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2019 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 System.Windows;
+using System.Windows.Controls;
+using System.Windows.Documents;
+using System.Windows.Media;
+using SafeExamBrowser.Contracts.I18n;
+using SafeExamBrowser.Contracts.Logging;
+
+namespace SafeExamBrowser.UserInterface.Mobile.ViewModels
+{
+ internal class LogViewModel : ILogObserver
+ {
+ private IText text;
+ private ScrollViewer scrollViewer;
+ private TextBlock textBlock;
+
+ public string WindowTitle => text.Get(TextKey.LogWindow_Title);
+
+ public LogViewModel(IText text, ScrollViewer scrollViewer, TextBlock textBlock)
+ {
+ this.text = text;
+ this.scrollViewer = scrollViewer;
+ this.textBlock = textBlock;
+ }
+
+ public void Notify(ILogContent content)
+ {
+ switch (content)
+ {
+ case ILogText text:
+ AppendLogText(text);
+ break;
+ case ILogMessage message:
+ AppendLogMessage(message);
+ break;
+ default:
+ throw new NotImplementedException($"The log window is not yet implemented for log content of type {content.GetType()}!");
+ }
+
+ scrollViewer.Dispatcher.Invoke(scrollViewer.ScrollToEnd);
+ }
+
+ private void AppendLogText(ILogText logText)
+ {
+ textBlock.Dispatcher.Invoke(() =>
+ {
+ var isHeader = logText.Text.StartsWith("/* ");
+ var isComment = logText.Text.StartsWith("# ");
+ var brush = isHeader || isComment ? Brushes.LimeGreen : textBlock.Foreground;
+
+ textBlock.Inlines.Add(new Run($"{logText.Text}{Environment.NewLine}")
+ {
+ FontWeight = isHeader ? FontWeights.Bold : FontWeights.Normal,
+ Foreground = brush
+ });
+ });
+ }
+
+ private void AppendLogMessage(ILogMessage message)
+ {
+ textBlock.Dispatcher.Invoke(() =>
+ {
+ var date = message.DateTime.ToString("HH:mm:ss.fff");
+ var severity = message.Severity.ToString().ToUpper();
+ var threadId = message.ThreadInfo.Id < 10 ? $"0{message.ThreadInfo.Id}" : message.ThreadInfo.Id.ToString();
+ var threadName = message.ThreadInfo.HasName ? ": " + message.ThreadInfo.Name : string.Empty;
+ var threadInfo = $"[{threadId}{threadName}]";
+
+ var infoRun = new Run($"{date} {threadInfo} - ") { Foreground = Brushes.DarkGray };
+ var messageRun = new Run($"{severity}: {message.Message}{Environment.NewLine}") { Foreground = GetBrushFor(message.Severity) };
+
+ textBlock.Inlines.Add(infoRun);
+ textBlock.Inlines.Add(messageRun);
+ });
+ }
+
+ private Brush GetBrushFor(LogLevel severity)
+ {
+ switch (severity)
+ {
+ case LogLevel.Debug:
+ return Brushes.DarkGray;
+ case LogLevel.Error:
+ return Brushes.Red;
+ case LogLevel.Warning:
+ return Brushes.Orange;
+ default:
+ return Brushes.White;
+ }
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/ViewModels/ProgressIndicatorViewModel.cs b/SafeExamBrowser.UserInterface.Mobile/ViewModels/ProgressIndicatorViewModel.cs
new file mode 100644
index 00000000..1b3ba6e6
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/ViewModels/ProgressIndicatorViewModel.cs
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2019 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.Mobile.ViewModels
+{
+ internal class ProgressIndicatorViewModel : INotifyPropertyChanged
+ {
+ private readonly object @lock = new object();
+
+ private Timer busyTimer;
+ private int currentProgress;
+ private bool isIndeterminate;
+ private int maxProgress;
+ private string status;
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public bool BusyIndication
+ {
+ set
+ {
+ HandleBusyIndication(value);
+ }
+ }
+
+ public int CurrentProgress
+ {
+ get
+ {
+ return currentProgress;
+ }
+ set
+ {
+ currentProgress = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentProgress)));
+ }
+ }
+
+ public bool IsIndeterminate
+ {
+ get
+ {
+ return isIndeterminate;
+ }
+ set
+ {
+ isIndeterminate = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsIndeterminate)));
+ }
+ }
+
+ public int MaxProgress
+ {
+ get
+ {
+ return maxProgress;
+ }
+ set
+ {
+ maxProgress = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MaxProgress)));
+ }
+ }
+
+ public string Status
+ {
+ get
+ {
+ return status;
+ }
+ set
+ {
+ status = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Status)));
+ }
+ }
+
+ protected void OnPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ private void HandleBusyIndication(bool start)
+ {
+ lock (@lock)
+ {
+ if (busyTimer != null)
+ {
+ busyTimer.Elapsed -= BusyTimer_Elapsed;
+ busyTimer.Stop();
+ busyTimer.Close();
+ }
+
+ if (start)
+ {
+ busyTimer = new Timer
+ {
+ AutoReset = true,
+ Interval = 1500,
+ };
+ busyTimer.Elapsed += BusyTimer_Elapsed;
+ busyTimer.Start();
+ }
+ }
+ }
+
+ private void BusyTimer_Elapsed(object sender, ElapsedEventArgs e)
+ {
+ var next = Status ?? string.Empty;
+
+ if (next.EndsWith("..."))
+ {
+ next = Status.Substring(0, Status.Length - 3);
+ }
+ else
+ {
+ next += ".";
+ }
+
+ Status = next;
+ busyTimer.Interval = 750;
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/ViewModels/RuntimeWindowViewModel.cs b/SafeExamBrowser.UserInterface.Mobile/ViewModels/RuntimeWindowViewModel.cs
new file mode 100644
index 00000000..c57f84a8
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/ViewModels/RuntimeWindowViewModel.cs
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2019 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 System.Windows;
+using System.Windows.Controls;
+using System.Windows.Documents;
+using System.Windows.Media;
+using SafeExamBrowser.Contracts.Logging;
+
+namespace SafeExamBrowser.UserInterface.Mobile.ViewModels
+{
+ internal class RuntimeWindowViewModel : ProgressIndicatorViewModel
+ {
+ private Visibility animatedBorderVisibility, progressBarVisibility;
+ private TextBlock textBlock;
+
+ public Visibility AnimatedBorderVisibility
+ {
+ get
+ {
+ return animatedBorderVisibility;
+ }
+ set
+ {
+ animatedBorderVisibility = value;
+ OnPropertyChanged(nameof(AnimatedBorderVisibility));
+ }
+ }
+
+ public Visibility ProgressBarVisibility
+ {
+ get
+ {
+ return progressBarVisibility;
+ }
+ set
+ {
+ progressBarVisibility = value;
+ OnPropertyChanged(nameof(ProgressBarVisibility));
+ }
+ }
+
+ public RuntimeWindowViewModel(TextBlock textBlock)
+ {
+ this.textBlock = textBlock;
+ }
+
+ public void Notify(ILogContent content)
+ {
+ switch (content)
+ {
+ case ILogText text:
+ AppendLogText(text);
+ break;
+ case ILogMessage message:
+ AppendLogMessage(message);
+ break;
+ default:
+ throw new NotImplementedException($"The runtime window is not yet implemented for log content of type {content.GetType()}!");
+ }
+ }
+
+ private void AppendLogMessage(ILogMessage message)
+ {
+ var time = message.DateTime.ToString("HH:mm:ss.fff");
+ var severity = message.Severity.ToString().ToUpper();
+
+ var infoRun = new Run($"{time} - ") { Foreground = Brushes.Gray };
+ var messageRun = new Run($"{severity}: {message.Message}{Environment.NewLine}") { Foreground = GetBrushFor(message.Severity) };
+
+ textBlock.Inlines.Add(infoRun);
+ textBlock.Inlines.Add(messageRun);
+ }
+
+ private void AppendLogText(ILogText text)
+ {
+ textBlock.Inlines.Add(new Run($"{text.Text}{Environment.NewLine}"));
+ }
+
+ private Brush GetBrushFor(LogLevel severity)
+ {
+ switch (severity)
+ {
+ case LogLevel.Debug:
+ return Brushes.Gray;
+ case LogLevel.Error:
+ return Brushes.Red;
+ case LogLevel.Warning:
+ return Brushes.Orange;
+ default:
+ return Brushes.Black;
+ }
+ }
+ }
+}
diff --git a/SafeExamBrowser.UserInterface.Mobile/packages.config b/SafeExamBrowser.UserInterface.Mobile/packages.config
new file mode 100644
index 00000000..2b94d949
--- /dev/null
+++ b/SafeExamBrowser.UserInterface.Mobile/packages.config
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/SafeExamBrowser.sln b/SafeExamBrowser.sln
index f61f74d6..c586616c 100644
--- a/SafeExamBrowser.sln
+++ b/SafeExamBrowser.sln
@@ -54,6 +54,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SafeExamBrowser.Logging.Uni
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SafeExamBrowser.Configuration.UnitTests", "SafeExamBrowser.Configuration.UnitTests\SafeExamBrowser.Configuration.UnitTests.csproj", "{9CDD03E7-ED65-409F-8C07-BD6F633A0170}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SafeExamBrowser.UserInterface.Mobile", "SafeExamBrowser.UserInterface.Mobile\SafeExamBrowser.UserInterface.Mobile.csproj", "{89BC24DD-FF31-496E-9816-A160B686A3D4}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -222,6 +224,14 @@ Global
{9CDD03E7-ED65-409F-8C07-BD6F633A0170}.Release|Any CPU.Build.0 = Release|Any CPU
{9CDD03E7-ED65-409F-8C07-BD6F633A0170}.Release|x86.ActiveCfg = Release|x86
{9CDD03E7-ED65-409F-8C07-BD6F633A0170}.Release|x86.Build.0 = Release|x86
+ {89BC24DD-FF31-496E-9816-A160B686A3D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {89BC24DD-FF31-496E-9816-A160B686A3D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {89BC24DD-FF31-496E-9816-A160B686A3D4}.Debug|x86.ActiveCfg = Debug|x86
+ {89BC24DD-FF31-496E-9816-A160B686A3D4}.Debug|x86.Build.0 = Debug|x86
+ {89BC24DD-FF31-496E-9816-A160B686A3D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {89BC24DD-FF31-496E-9816-A160B686A3D4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {89BC24DD-FF31-496E-9816-A160B686A3D4}.Release|x86.ActiveCfg = Release|Any CPU
+ {89BC24DD-FF31-496E-9816-A160B686A3D4}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE