Restore SEBPatch

This commit is contained in:
2025-06-01 11:44:20 +02:00
commit 8c656e3137
1297 changed files with 142172 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
<Window x:Class="SafeExamBrowser.UserInterface.Mobile.Windows.AboutWindow" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile"
mc:Ignorable="d" Background="White" Height="450" Width="675" ResizeMode="NoResize" Icon="../Images/SafeExamBrowser.ico" FontSize="16"
ShowInTaskbar="False" WindowStartupLocation="CenterScreen">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Templates/ScrollViewers.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="1.2*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Image Grid.ColumnSpan="2" Source="pack://application:,,,/SafeExamBrowser.UserInterface.Mobile;component/Images/SplashScreen.png" Margin="0,5,0,0" />
<TextBlock x:Name="VersionInfo" Grid.Row="0" Grid.Column="1" Foreground="DimGray" Margin="35,95,100,10" TextWrapping="Wrap" />
<ScrollViewer Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Template="{StaticResource SmallBarScrollViewer}" VerticalScrollBarVisibility="Auto">
<TextBlock x:Name="MainText" Margin="10" TextWrapping="Wrap">
<LineBreak />
<LineBreak />
<Bold><Underline>.NET Framework</Underline></Bold>
<LineBreak />
Copyright © 2002-2024 Microsoft. All rights reserved.
<LineBreak />
<LineBreak />
<Bold><Underline>CefSharp (.NET bindings for the Chromium Embedded Framework)</Underline></Bold>
<LineBreak />
Copyright © 2010-2024 The CefSharp Authors. All rights reserved.
<LineBreak />
<LineBreak />
<Bold><Underline>CEF (Chromium Embedded Framework)</Underline></Bold>
<LineBreak />
Copyright © 2008-2024 The Chromium Embedded Framework Authors. All rights reserved.
<LineBreak />
<LineBreak />
<Bold><Underline>Font-Awesome-WPF</Underline></Bold>
<LineBreak />
Copyright © 2014-2024 charri. All rights reserved.
<LineBreak />
<LineBreak />
<Bold><Underline>KGy SOFT Core &amp; Drawing Libraries</Underline></Bold>
<LineBreak />
Copyright © 2005-2024 KGy SOFT. All rights reserved.
<LineBreak />
<LineBreak />
<Bold><Underline>KnownFolders</Underline></Bold>
<LineBreak />
Copyright © 2017-2024 Syroot. All rights reserved.
<LineBreak />
<LineBreak />
<Bold><Underline>Microsoft.Windows.SDK.Contracts</Underline></Bold>
<LineBreak />
Copyright © 2019-2024 Microsoft. All rights reserved.
<LineBreak />
<LineBreak />
<Bold><Underline>NAudio</Underline></Bold>
<LineBreak />
Copyright © 2008-2024 Mark Heath &amp; contributors. All rights reserved.
<LineBreak />
<LineBreak />
<Bold><Underline>Newtonsoft.Json</Underline></Bold>
<LineBreak />
Copyright © 2007-2024 James Newton-King. All rights reserved.
<LineBreak />
<LineBreak />
<Bold><Underline>Visual C++ Redistributable</Underline></Bold>
<LineBreak />
Copyright © 1993-2024 Microsoft. All rights reserved.
</TextBlock>
</ScrollViewer>
</Grid>
</Window>

View File

@@ -0,0 +1,68 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* 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 System.Windows.Media;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Windows;
using SafeExamBrowser.UserInterface.Contracts.Windows.Events;
namespace SafeExamBrowser.UserInterface.Mobile.Windows
{
internal partial class AboutWindow : Window, IWindow
{
private readonly AppConfig appConfig;
private readonly IText text;
private WindowClosedEventHandler closed;
private WindowClosingEventHandler closing;
event WindowClosedEventHandler IWindow.Closed
{
add { closed += value; }
remove { closed -= value; }
}
event WindowClosingEventHandler IWindow.Closing
{
add { closing += value; }
remove { closing -= value; }
}
internal AboutWindow(AppConfig appConfig, IText text)
{
this.appConfig = appConfig;
this.text = text;
InitializeComponent();
InitializeAboutWindow();
}
public void BringToForeground()
{
Activate();
}
private void InitializeAboutWindow()
{
Closed += (o, args) => closed?.Invoke();
Closing += (o, args) => closing?.Invoke();
MainText.Inlines.InsertBefore(MainText.Inlines.FirstInline, new Run(text.Get(TextKey.AboutWindow_LicenseInfo)));
Title = text.Get(TextKey.AboutWindow_Title);
VersionInfo.Inlines.Add(new Run($"{text.Get(TextKey.Version)} {appConfig.ProgramInformationalVersion}"));
VersionInfo.Inlines.Add(new LineBreak());
VersionInfo.Inlines.Add(new Run($"{text.Get(TextKey.Build)} {appConfig.ProgramBuildVersion}") { FontSize = 10, Foreground = Brushes.Gray });
VersionInfo.Inlines.Add(new LineBreak());
VersionInfo.Inlines.Add(new LineBreak());
VersionInfo.Inlines.Add(new Run(appConfig.ProgramCopyright) { FontSize = 12, Foreground = Brushes.Gray });
}
}
}

View File

@@ -0,0 +1,30 @@
<Window x:Class="SafeExamBrowser.UserInterface.Mobile.Windows.ActionCenter" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile.Controls.ActionCenter"
mc:Ignorable="d" Title="ActionCenter" Height="1000" Width="400" Background="{DynamicResource BackgroundTransparentBrush}"
AllowsTransparency="True" FontSize="16" WindowStyle="None" Topmost="True" ResizeMode="NoResize">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Templates/Colors.xaml" />
<ResourceDictionary Source="../Templates/ScrollViewers.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ScrollViewer Grid.Row="0" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" Template="{StaticResource SmallBarScrollViewer}">
<StackPanel x:Name="ApplicationPanel" Orientation="Vertical" />
</ScrollViewer>
<UniformGrid x:Name="ControlPanel" Grid.Row="1" Columns="3" Margin="10">
<local:Clock x:Name="Clock" />
<local:QuitButton x:Name="QuitButton" />
</UniformGrid>
</Grid>
</Window>

View File

@@ -0,0 +1,219 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* 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;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Animation;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Shell;
using SafeExamBrowser.UserInterface.Contracts.Shell.Events;
namespace SafeExamBrowser.UserInterface.Mobile.Windows
{
internal partial class ActionCenter : Window, IActionCenter
{
public bool ShowClock
{
set { Dispatcher.Invoke(() => Clock.Visibility = value ? Visibility.Visible : Visibility.Collapsed); }
}
public bool ShowQuitButton
{
set { Dispatcher.Invoke(() => QuitButton.Visibility = value ? Visibility.Visible : Visibility.Collapsed); }
}
public event QuitButtonClickedEventHandler QuitButtonClicked;
internal ActionCenter()
{
InitializeComponent();
InitializeActionCenter();
}
public void AddApplicationControl(IApplicationControl control, bool atFirstPosition = false)
{
if (control is UIElement uiElement)
{
if (atFirstPosition)
{
ApplicationPanel.Children.Insert(0, uiElement);
}
else
{
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 Promote()
{
Task.Run(() =>
{
Dispatcher.Invoke(ShowAnimated);
Thread.Sleep(2000);
Dispatcher.Invoke(HideAnimated);
});
}
public void Register(IActionCenterActivator activator)
{
activator.Activated += Activator_Activated;
activator.Deactivated += Activator_Deactivated;
activator.Toggled += Activator_Toggled;
}
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_Activated()
{
Dispatcher.InvokeAsync(() =>
{
if (Visibility != Visibility.Visible)
{
ShowAnimated();
}
});
}
private void Activator_Deactivated()
{
Dispatcher.InvokeAsync(() =>
{
if (Visibility == Visibility.Visible)
{
HideAnimated();
}
});
}
private void Activator_Toggled()
{
Dispatcher.InvokeAsync(() =>
{
if (Visibility != Visibility.Visible)
{
ShowAnimated();
}
else
{
HideAnimated();
}
});
}
private void InitializeActionCenter()
{
QuitButton.Clicked += (args) => QuitButtonClicked?.Invoke(args);
}
}
}

View File

@@ -0,0 +1,128 @@
<Window x:Class="SafeExamBrowser.UserInterface.Mobile.Windows.BrowserWindow" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:fa="http://schemas.fontawesome.io/icons/"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile.Controls"
mc:Ignorable="d" Background="#FFF0F0F0" FontSize="16" Height="500" Width="750" MinHeight="250" MinWidth="250" Icon="../Images/SafeExamBrowser.ico">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Templates/Buttons.xaml" />
<ResourceDictionary Source="../Templates/Colors.xaml" />
<ResourceDictionary Source="../Templates/ScrollViewers.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border x:Name="Toolbar" Grid.Row="0" BorderBrush="LightGray" BorderThickness="0,0,0,1" Margin="5,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0" x:Name="HomeButton" Height="50" HorizontalAlignment="Center" Margin="5" Template="{StaticResource BrowserButton}" VerticalAlignment="Center" />
<Button Grid.Column="1" x:Name="BackwardButton" Height="50" HorizontalAlignment="Center" Margin="5" Template="{StaticResource BrowserButton}" VerticalAlignment="Center" />
<Button Grid.Column="2" x:Name="ForwardButton" Height="50" HorizontalAlignment="Center" Margin="5" Template="{StaticResource BrowserButton}" VerticalAlignment="Center" />
<Button Grid.Column="3" x:Name="ReloadButton" Height="50" HorizontalAlignment="Center" Margin="5" Template="{StaticResource BrowserButton}" VerticalAlignment="Center" />
<TextBox Grid.Column="4" x:Name="UrlTextBox" Height="50" HorizontalAlignment="Stretch" Margin="5,10" Padding="8" VerticalContentAlignment="Center" />
<Button Grid.Column="5" x:Name="DownloadsButton" Height="50" HorizontalAlignment="Center" Margin="5" Padding="5" Template="{StaticResource BrowserButton}" VerticalAlignment="Center" Visibility="Collapsed">
<fa:ImageAwesome Icon="Download" />
</Button>
<Popup x:Name="DownloadsPopup" AllowsTransparency="True" PopupAnimation="Slide" Placement="Custom" PlacementTarget="{Binding ElementName=BrowserControlHost}">
<Border Background="{StaticResource BackgroundBrush}" BorderBrush="LightGray" BorderThickness="1,0,1,1" MinWidth="250">
<StackPanel x:Name="Downloads" Orientation="Vertical" />
</Border>
</Popup>
<Button Grid.Column="6" x:Name="MenuButton" Height="50" HorizontalAlignment="Center" Margin="5" Template="{StaticResource BrowserButton}" VerticalAlignment="Center" />
<Popup x:Name="MenuPopup" IsOpen="False" AllowsTransparency="True" PopupAnimation="Slide" Placement="Custom" PlacementTarget="{Binding ElementName=BrowserControlHost}">
<Border Background="{StaticResource BackgroundBrush}" BorderBrush="LightGray" BorderThickness="1,0,1,1" Width="350">
<StackPanel Orientation="Vertical">
<Grid x:Name="ZoomMenuItem" Height="55">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" x:Name="ZoomText" HorizontalAlignment="Left" Margin="10,0" VerticalAlignment="Center" />
<Button Grid.Column="1" x:Name="ZoomInButton" Margin="5" Padding="5" Template="{StaticResource BrowserButton}" TabIndex="30">
<fa:ImageAwesome Icon="SearchPlus" />
</Button>
<Button Grid.Column="2" x:Name="ZoomResetButton" Margin="5" Padding="5" Template="{StaticResource BrowserButton}" TabIndex="31">
<Viewbox Stretch="Uniform">
<TextBlock x:Name="ZoomLevel" />
</Viewbox>
</Button>
<Button Grid.Column="3" x:Name="ZoomOutButton" Margin="5" Padding="5" Template="{StaticResource BrowserButton}" TabIndex="32">
<fa:ImageAwesome Icon="SearchMinus" />
</Button>
</Grid>
<Grid x:Name="FindMenuItem" Height="55">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" x:Name="FindMenuText" HorizontalAlignment="Left" Margin="10,0" VerticalAlignment="Center" />
<Button Grid.Column="1" x:Name="FindMenuButton" Margin="5" Padding="5" Template="{StaticResource BrowserButton}" TabIndex="33">
<fa:ImageAwesome Icon="Search" />
</Button>
</Grid>
<Grid x:Name="DeveloperConsoleMenuItem" Height="55">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" x:Name="DeveloperConsoleText" HorizontalAlignment="Left" Margin="10,0" VerticalAlignment="Center" />
<Button Grid.Column="1" x:Name="DeveloperConsoleButton" Margin="5" Padding="5" Template="{StaticResource BrowserButton}" TabIndex="34">
<fa:ImageAwesome Icon="Wrench" />
</Button>
</Grid>
</StackPanel>
</Border>
</Popup>
</Grid>
<ProgressBar Grid.Row="1" x:Name="ProgressBar" Background="Transparent" BorderThickness="0" Foreground="DodgerBlue" Height="2" Margin="0,0,0,-1" />
</Grid>
</Border>
<WindowsFormsHost Grid.Row="1" x:Name="BrowserControlHost" />
<Border x:Name="Findbar" Grid.Row="2" BorderBrush="LightGray" BorderThickness="0,1,0,0" Margin="5,0" Visibility="Collapsed">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal" Height="50" Margin="5,0">
<TextBox x:Name="FindTextBox" Padding="5,0" VerticalContentAlignment="Center" Width="200" />
<Button x:Name="FindPreviousButton" Cursor="Hand" Padding="5" VerticalContentAlignment="Center" Width="50">
<fa:ImageAwesome Icon="AngleUp" />
</Button>
<Button x:Name="FindNextButton" Cursor="Hand" Padding="5" VerticalContentAlignment="Center" Width="50">
<fa:ImageAwesome Icon="AngleDown" />
</Button>
<CheckBox Grid.Column="1" x:Name="FindCaseSensitiveCheckBox" Cursor="Hand" Margin="10,0" Padding="5,0" VerticalContentAlignment="Center" />
</StackPanel>
<Button Grid.Column="4" x:Name="FindbarCloseButton" Height="50" HorizontalAlignment="Center" Margin="5" Padding="10" Template="{StaticResource BrowserButton}" VerticalAlignment="Center">
<fa:ImageAwesome Icon="Close" />
</Button>
</Grid>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,646 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* 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.Interop;
using System.Windows.Media.Imaging;
using SafeExamBrowser.Browser.Contracts.Events;
using SafeExamBrowser.Core.Contracts.Resources.Icons;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Settings.Browser;
using SafeExamBrowser.UserInterface.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Browser;
using SafeExamBrowser.UserInterface.Contracts.Browser.Data;
using SafeExamBrowser.UserInterface.Contracts.Browser.Events;
using SafeExamBrowser.UserInterface.Contracts.Windows;
using SafeExamBrowser.UserInterface.Contracts.Windows.Events;
using SafeExamBrowser.UserInterface.Mobile.Controls.Browser;
using SafeExamBrowser.UserInterface.Shared.Utilities;
namespace SafeExamBrowser.UserInterface.Mobile.Windows
{
internal partial class BrowserWindow : Window, IBrowserWindow
{
private const string CLEAR_FIND_TERM = "thisisahacktoclearthesearchresultsasitappearsthatthereisnosuchfunctionalityincef";
private readonly bool isMainWindow;
private readonly BrowserSettings settings;
private readonly IText text;
private readonly ILogger logger;
private readonly IBrowserControl browserControl;
private WindowClosedEventHandler closed;
private WindowClosingEventHandler closing;
private bool browserControlGetsFocusFromTaskbar = false;
private IInputElement tabKeyDownFocusElement = null;
private WindowSettings WindowSettings
{
get { return isMainWindow ? settings.MainWindow : settings.AdditionalWindow; }
}
public bool CanNavigateBackwards { set => Dispatcher.Invoke(() => BackwardButton.IsEnabled = value); }
public bool CanNavigateForwards { set => Dispatcher.Invoke(() => ForwardButton.IsEnabled = value); }
public IntPtr Handle { get; private set; }
public event AddressChangedEventHandler AddressChanged;
public event ActionRequestedEventHandler BackwardNavigationRequested;
public event ActionRequestedEventHandler DeveloperConsoleRequested;
public event FindRequestedEventHandler FindRequested;
public event ActionRequestedEventHandler ForwardNavigationRequested;
public event ActionRequestedEventHandler HomeNavigationRequested;
public event ActionRequestedEventHandler ReloadRequested;
public event ActionRequestedEventHandler ZoomInRequested;
public event ActionRequestedEventHandler ZoomOutRequested;
public event ActionRequestedEventHandler ZoomResetRequested;
public event LoseFocusRequestedEventHandler LoseFocusRequested;
event WindowClosedEventHandler IWindow.Closed
{
add { closed += value; }
remove { closed -= value; }
}
event WindowClosingEventHandler IWindow.Closing
{
add { closing += value; }
remove { closing -= value; }
}
internal BrowserWindow(IBrowserControl browserControl, BrowserSettings settings, bool isMainWindow, IText text, ILogger logger)
{
this.isMainWindow = isMainWindow;
this.settings = settings;
this.text = text;
this.logger = logger;
this.browserControl = browserControl;
InitializeComponent();
InitializeBrowserWindow(browserControl);
}
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 void FocusToolbar(bool forward)
{
Dispatcher.BeginInvoke((Action) (async () =>
{
Activate();
await Task.Delay(50);
// focus all elements in the toolbar, such that the last element that is enabled gets focus
var buttons = new System.Windows.Controls.Control[] { ForwardButton, BackwardButton, ReloadButton, UrlTextBox, MenuButton, };
for (var i = forward ? 0 : buttons.Length - 1; i >= 0 && i < buttons.Length; i += forward ? 1 : -1)
{
if (buttons[i].IsEnabled && buttons[i].Visibility == Visibility.Visible)
{
buttons[i].Focus();
break;
}
}
}));
}
public void FocusBrowser()
{
Dispatcher.BeginInvoke((Action) (async () =>
{
FocusToolbar(false);
await Task.Delay(100);
browserControlGetsFocusFromTaskbar = true;
var focusedElement = FocusManager.GetFocusedElement(this) as UIElement;
focusedElement.MoveFocus(new TraversalRequest(FocusNavigationDirection.Right));
await Task.Delay(150);
browserControlGetsFocusFromTaskbar = false;
}));
}
public void FocusAddressBar()
{
Dispatcher.BeginInvoke((Action) (() =>
{
UrlTextBox.Focus();
}));
}
public new void Hide()
{
Dispatcher.Invoke(base.Hide);
}
public new void Show()
{
Dispatcher.Invoke(base.Show);
}
public void ShowFindbar()
{
Dispatcher.InvokeAsync(() =>
{
Findbar.Visibility = Visibility.Visible;
FindTextBox.Focus();
});
}
public void UpdateAddress(string url)
{
Dispatcher.Invoke(() => UrlTextBox.Text = url);
}
public void UpdateIcon(IconResource icon)
{
Dispatcher.InvokeAsync(() =>
{
if (icon is BitmapIconResource bitmap)
{
Icon = new BitmapImage(bitmap.Uri);
}
});
}
public void UpdateDownloadState(DownloadItemState state)
{
Dispatcher.InvokeAsync(() =>
{
var isNewItem = true;
foreach (var child in Downloads.Children)
{
if (child is DownloadItemControl control && control.Id == state.Id)
{
control.Update(state);
isNewItem = false;
break;
}
}
if (isNewItem)
{
var control = new DownloadItemControl(state.Id, text);
control.Update(state);
Downloads.Children.Add(control);
}
DownloadsButton.Visibility = Visibility.Visible;
DownloadsPopup.IsOpen = IsActive;
});
}
public void UpdateLoadingState(bool isLoading)
{
Dispatcher.Invoke(() => ProgressBar.Visibility = isLoading ? Visibility.Visible : Visibility.Hidden);
}
public void UpdateProgress(double value)
{
Dispatcher.Invoke(() => ProgressBar.Value = value * 100);
}
public void UpdateTitle(string title)
{
Dispatcher.Invoke(() => Title = title);
}
public void UpdateZoomLevel(double value)
{
Dispatcher.Invoke(() =>
{
ZoomLevel.Text = $"{value}%";
var zoomButtonName = text.Get(TextKey.BrowserWindow_ZoomLevelReset).Replace("%%ZOOM%%", value.ToString("0"));
ZoomResetButton.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, zoomButtonName);
});
}
private void BrowserWindow_Closing(object sender, CancelEventArgs e)
{
if (isMainWindow)
{
e.Cancel = true;
}
else
{
closing?.Invoke();
}
}
private void BrowserWindow_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Tab)
{
var hasShift = (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift;
if (Toolbar.IsKeyboardFocusWithin && hasShift)
{
var firstActiveElementInToolbar = Toolbar.PredictFocus(FocusNavigationDirection.Right);
if (firstActiveElementInToolbar is System.Windows.UIElement)
{
var control = firstActiveElementInToolbar as System.Windows.UIElement;
if (control.IsKeyboardFocusWithin)
{
LoseFocusRequested?.Invoke(false);
e.Handled = true;
}
}
}
tabKeyDownFocusElement = FocusManager.GetFocusedElement(this);
}
else
{
tabKeyDownFocusElement = null;
}
}
private void BrowserWindow_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.F5)
{
ReloadRequested?.Invoke();
}
if (e.Key == Key.Home)
{
HomeNavigationRequested?.Invoke();
}
if (settings.AllowFind && (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) && e.Key == Key.F)
{
ShowFindbar();
}
if (e.Key == Key.Tab)
{
var hasCtrl = (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control;
var hasShift = (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift;
if (BrowserControlHost.IsFocused && hasCtrl)
{
if (Findbar.Visibility == Visibility.Hidden || hasShift)
{
Toolbar.Focus();
}
else if (Toolbar.Visibility == Visibility.Hidden)
{
Findbar.Focus();
}
}
else if (MenuPopup.IsKeyboardFocusWithin)
{
var focusedElement = FocusManager.GetFocusedElement(this);
var focusedControl = focusedElement as System.Windows.Controls.Control;
var prevFocusedControl = tabKeyDownFocusElement as System.Windows.Controls.Control;
if (focusedControl != null && prevFocusedControl != null)
{
if (!hasShift && focusedControl.TabIndex < prevFocusedControl.TabIndex)
{
MenuPopup.IsOpen = false;
FocusBrowser();
}
else if (hasShift && focusedControl.TabIndex > prevFocusedControl.TabIndex)
{
MenuPopup.IsOpen = false;
MenuButton.Focus();
}
}
}
}
if (e.Key == Key.Escape && MenuPopup.IsOpen)
{
MenuPopup.IsOpen = false;
MenuButton.Focus();
}
}
private void BrowserWindow_Loaded(object sender, RoutedEventArgs e)
{
Handle = new WindowInteropHelper(this).Handle;
if (isMainWindow)
{
this.DisableCloseButton();
}
}
private void FindbarCloseButton_Click(object sender, RoutedEventArgs e)
{
FindRequested?.Invoke(CLEAR_FIND_TERM, true, false);
Findbar.Visibility = Visibility.Collapsed;
}
private void FindNextButton_Click(object sender, RoutedEventArgs e)
{
FindRequested?.Invoke(FindTextBox.Text, false, FindCaseSensitiveCheckBox.IsChecked == true);
}
private void FindPreviousButton_Click(object sender, RoutedEventArgs e)
{
FindRequested?.Invoke(FindTextBox.Text, false, FindCaseSensitiveCheckBox.IsChecked == true, false);
}
private void FindTextBox_KeyUp(object sender, KeyEventArgs e)
{
if (string.IsNullOrEmpty(FindTextBox.Text))
{
FindRequested?.Invoke(CLEAR_FIND_TERM, true, false);
}
else if (e.Key == Key.Enter)
{
FindRequested?.Invoke(FindTextBox.Text, false, FindCaseSensitiveCheckBox.IsChecked == true);
}
else
{
FindRequested?.Invoke(FindTextBox.Text, true, FindCaseSensitiveCheckBox.IsChecked == true);
}
}
private CustomPopupPlacement[] Popup_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.EmbeddableControl is System.Windows.Forms.Control control)
{
BrowserControlHost.Child = control;
}
RegisterEvents();
InitializeBounds();
ApplySettings();
LoadIcons();
LoadText();
}
private void RegisterEvents()
{
BackwardButton.Click += (o, args) => BackwardNavigationRequested?.Invoke();
Closed += (o, args) => closed?.Invoke();
Closing += BrowserWindow_Closing;
DeveloperConsoleButton.Click += (o, args) => DeveloperConsoleRequested?.Invoke();
DownloadsButton.Click += (o, args) => DownloadsPopup.IsOpen = !DownloadsPopup.IsOpen;
DownloadsButton.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => DownloadsPopup.IsOpen = DownloadsPopup.IsMouseOver));
DownloadsPopup.CustomPopupPlacementCallback = new CustomPopupPlacementCallback(Popup_PlacementCallback);
DownloadsPopup.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => DownloadsPopup.IsOpen = DownloadsPopup.IsMouseOver));
FindbarCloseButton.Click += FindbarCloseButton_Click;
FindNextButton.Click += FindNextButton_Click;
FindPreviousButton.Click += FindPreviousButton_Click;
FindMenuButton.Click += (o, args) => ShowFindbar();
FindTextBox.KeyUp += FindTextBox_KeyUp;
ForwardButton.Click += (o, args) => ForwardNavigationRequested?.Invoke();
HomeButton.Click += (o, args) => HomeNavigationRequested?.Invoke();
Loaded += BrowserWindow_Loaded;
MenuButton.Click += MenuButton_Click;
MenuPopup.CustomPopupPlacementCallback = new CustomPopupPlacementCallback(Popup_PlacementCallback);
MenuPopup.LostFocus += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => MenuPopup.IsOpen = MenuPopup.IsKeyboardFocusWithin));
KeyDown += BrowserWindow_KeyDown;
KeyUp += BrowserWindow_KeyUp;
LocationChanged += (o, args) => { DownloadsPopup.IsOpen = false; MenuPopup.IsOpen = false; };
ReloadButton.Click += (o, args) => ReloadRequested?.Invoke();
SizeChanged += (o, args) => { DownloadsPopup.IsOpen = false; MenuPopup.IsOpen = false; };
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();
BrowserControlHost.GotKeyboardFocus += BrowserControlHost_GotKeyboardFocus;
}
private void MenuButton_Click(object sender, RoutedEventArgs e)
{
MenuPopup.IsOpen = !MenuPopup.IsOpen;
ZoomInButton.Focus();
}
private void BrowserControlHost_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
var forward = !browserControlGetsFocusFromTaskbar;
// focus the first / last element on the page
var javascript = @"
if (typeof __SEB_focusElement === 'undefined') {
__SEB_focusElement = function (forward) {
if (!document.body) { return; }
var items = [].map
.call(document.body.querySelectorAll(['input', 'select', 'a[href]', 'textarea', 'button', '[tabindex]']), function(el, i) { return { el, i } })
.filter(function(e) { return e.el.tabIndex >= 0 && !e.el.disabled && e.el.offsetParent; })
.sort(function(a,b) { return a.el.tabIndex === b.el.tabIndex ? a.i - b.i : (a.el.tabIndex || 9E9) - (b.el.tabIndex || 9E9); })
var item = items[forward ? 1 : items.length - 1];
if (item && item.focus && typeof item.focus !== 'function')
throw ('item.focus is not a function, ' + typeof item.focus)
setTimeout(function () { item && item.focus && item.focus(); }, 20);
}
}";
browserControl.ExecuteJavaScript(javascript, result =>
{
if (!result.Success)
{
logger.Warn($"Failed to initialize JavaScript: {result.Message}!");
}
});
browserControl.ExecuteJavaScript("__SEB_focusElement(" + forward.ToString().ToLower() + ")", result =>
{
if (!result.Success)
{
logger.Warn($"Failed to execute JavaScript: {result.Message}!");
}
});
}
private void ApplySettings()
{
BackwardButton.IsEnabled = WindowSettings.AllowBackwardNavigation;
BackwardButton.Visibility = WindowSettings.AllowBackwardNavigation ? Visibility.Visible : Visibility.Collapsed;
DeveloperConsoleMenuItem.Visibility = WindowSettings.AllowDeveloperConsole ? Visibility.Visible : Visibility.Collapsed;
FindMenuItem.Visibility = settings.AllowFind ? Visibility.Visible : Visibility.Collapsed;
ForwardButton.IsEnabled = WindowSettings.AllowForwardNavigation;
ForwardButton.Visibility = WindowSettings.AllowForwardNavigation ? Visibility.Visible : Visibility.Collapsed;
HomeButton.IsEnabled = WindowSettings.ShowHomeButton;
HomeButton.Visibility = WindowSettings.ShowHomeButton ? Visibility.Visible : Visibility.Collapsed;
ReloadButton.IsEnabled = WindowSettings.AllowReloading;
ReloadButton.Visibility = WindowSettings.ShowReloadButton ? Visibility.Visible : Visibility.Collapsed;
Toolbar.Visibility = WindowSettings.ShowToolbar ? Visibility.Visible : Visibility.Collapsed;
UrlTextBox.Visibility = WindowSettings.AllowAddressBar ? Visibility.Visible : Visibility.Hidden;
ZoomMenuItem.Visibility = settings.AllowPageZoom ? Visibility.Visible : Visibility.Collapsed;
if (!WindowSettings.AllowAddressBar)
{
BackwardButton.Height = 35;
ForwardButton.Height = 35;
ReloadButton.Height = 35;
UrlTextBox.Height = 20;
DownloadsButton.Height = 35;
MenuButton.Height = 35;
}
}
private void InitializeBounds()
{
if (isMainWindow && WindowSettings.FullScreenMode)
{
Top = 0;
Left = 0;
Height = SystemParameters.WorkArea.Height;
Width = SystemParameters.WorkArea.Width;
ResizeMode = ResizeMode.NoResize;
WindowStyle = WindowStyle.None;
}
else if (WindowSettings.RelativeHeight == 100 && WindowSettings.RelativeWidth == 100)
{
WindowState = WindowState.Maximized;
}
else
{
if (WindowSettings.RelativeHeight > 0)
{
Height = SystemParameters.WorkArea.Height * WindowSettings.RelativeHeight.Value / 100;
Top = (SystemParameters.WorkArea.Height / 2) - (Height / 2);
}
else if (WindowSettings.AbsoluteHeight > 0)
{
Height = this.TransformFromPhysical(0, WindowSettings.AbsoluteHeight.Value).Y;
Top = (SystemParameters.WorkArea.Height / 2) - (Height / 2);
}
if (WindowSettings.RelativeWidth > 0)
{
Width = SystemParameters.WorkArea.Width * WindowSettings.RelativeWidth.Value / 100;
}
else if (WindowSettings.AbsoluteWidth > 0)
{
Width = this.TransformFromPhysical(WindowSettings.AbsoluteWidth.Value, 0).X;
}
if (Height > SystemParameters.WorkArea.Height)
{
Top = 0;
Height = SystemParameters.WorkArea.Height;
}
if (Width > SystemParameters.WorkArea.Width)
{
Left = 0;
Width = SystemParameters.WorkArea.Width;
}
switch (WindowSettings.Position)
{
case WindowPosition.Left:
Left = 0;
break;
case WindowPosition.Center:
Left = (SystemParameters.WorkArea.Width / 2) - (Width / 2);
break;
case WindowPosition.Right:
Left = SystemParameters.WorkArea.Width - Width;
break;
}
}
}
private void LoadIcons()
{
var backward = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Mobile;component/Images/NavigateBack.xaml") };
var forward = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Mobile;component/Images/NavigateForward.xaml") };
var home = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Mobile;component/Images/Home.xaml") };
var menu = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Mobile;component/Images/Menu.xaml") };
var reload = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Mobile;component/Images/Reload.xaml") };
BackwardButton.Content = IconResourceLoader.Load(backward);
ForwardButton.Content = IconResourceLoader.Load(forward);
HomeButton.Content = IconResourceLoader.Load(home);
MenuButton.Content = IconResourceLoader.Load(menu);
ReloadButton.Content = IconResourceLoader.Load(reload);
}
private void LoadText()
{
DeveloperConsoleText.Text = text.Get(TextKey.BrowserWindow_DeveloperConsoleMenuItem);
DeveloperConsoleButton.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, text.Get(TextKey.BrowserWindow_DeveloperConsoleMenuItem));
FindCaseSensitiveCheckBox.Content = text.Get(TextKey.BrowserWindow_FindCaseSensitive);
FindMenuText.Text = text.Get(TextKey.BrowserWindow_FindMenuItem);
FindMenuButton.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, text.Get(TextKey.BrowserWindow_FindMenuItem));
ZoomText.Text = text.Get(TextKey.BrowserWindow_ZoomMenuItem);
ZoomInButton.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, text.Get(TextKey.BrowserWindow_ZoomMenuPlus));
ZoomOutButton.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, text.Get(TextKey.BrowserWindow_ZoomMenuMinus));
ReloadButton.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, text.Get(TextKey.BrowserWindow_ReloadButton));
BackwardButton.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, text.Get(TextKey.BrowserWindow_BackwardButton));
ForwardButton.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, text.Get(TextKey.BrowserWindow_ForwardButton));
DownloadsButton.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, text.Get(TextKey.BrowserWindow_DownloadsButton));
HomeButton.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, text.Get(TextKey.BrowserWindow_HomeButton));
MenuButton.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, text.Get(TextKey.BrowserWindow_MenuButton));
UrlTextBox.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, text.Get(TextKey.BrowserWindow_UrlTextBox));
}
}
}

View File

@@ -0,0 +1,56 @@
<Window x:Class="SafeExamBrowser.UserInterface.Mobile.Windows.CredentialsDialog" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fa="http://schemas.fontawesome.io/icons/"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile"
mc:Ignorable="d" Background="Transparent" Height="750" Width="1000" FontSize="16" ResizeMode="NoResize" Topmost="True" AllowsTransparency="True" WindowStyle="None">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Templates/Colors.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid Background="#66000000" FocusManager.FocusedElement="{Binding ElementName=Password}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" />
<Grid Grid.Row="1" Background="White">
<Grid Margin="25">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<fa:ImageAwesome Grid.Column="0" Name="PurposeIcon" Foreground="LightGray" Icon="Key" Margin="25" Rotation="90" Width="75" />
<Grid Grid.Column="1" Margin="25,0,25,25">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" x:Name="Message" Margin="0,20" TextWrapping="WrapWithOverflow" VerticalAlignment="Bottom" />
<StackPanel Grid.Row="1" Orientation="Vertical">
<Label Name="UsernameLabel" Padding="0,5" Target="{Binding ElementName=Username}" />
<TextBox Name="Username" Padding="12" VerticalContentAlignment="Center" />
</StackPanel>
<StackPanel Grid.Row="2" Orientation="Vertical">
<Label Name="PasswordLabel" Padding="0,5" Target="{Binding ElementName=Password}" />
<PasswordBox x:Name="Password" Padding="12" VerticalContentAlignment="Center" />
</StackPanel>
</Grid>
</Grid>
</Grid>
<Grid Grid.Row="2" Background="{StaticResource BackgroundBrush}">
<WrapPanel Orientation="Horizontal" Margin="50,25" HorizontalAlignment="Right" VerticalAlignment="Center">
<Button x:Name="ConfirmButton" Cursor="Hand" Margin="20,0" Padding="20,10" MinWidth="100" />
<Button x:Name="CancelButton" Cursor="Hand" Padding="20,10" MinWidth="100" />
</WrapPanel>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,148 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* 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 FontAwesome.WPF;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Windows;
using SafeExamBrowser.UserInterface.Contracts.Windows.Data;
using SafeExamBrowser.UserInterface.Contracts.Windows.Events;
namespace SafeExamBrowser.UserInterface.Mobile.Windows
{
internal partial class CredentialsDialog : Window, ICredentialsDialog
{
private readonly IText text;
private WindowClosedEventHandler closed;
private WindowClosingEventHandler closing;
event WindowClosedEventHandler IWindow.Closed
{
add { closed += value; }
remove { closed -= value; }
}
event WindowClosingEventHandler IWindow.Closing
{
add { closing += value; }
remove { closing -= value; }
}
internal CredentialsDialog(CredentialsDialogPurpose purpose, string message, string title, IText text)
{
this.text = text;
InitializeComponent();
InitializeCredentialsDialog(purpose, message, title);
}
public void BringToForeground()
{
Dispatcher.Invoke(Activate);
}
public CredentialsDialogResult Show(IWindow parent = null)
{
return Dispatcher.Invoke(() =>
{
var result = new CredentialsDialogResult { Success = false };
if (parent is Window)
{
Owner = parent as Window;
}
InitializeBounds();
if (ShowDialog() is true)
{
result.Password = Password.Password;
result.Success = true;
result.Username = Username.Text;
}
return result;
});
}
private void InitializeBounds()
{
Left = 0;
Top = 0;
Height = SystemParameters.PrimaryScreenHeight;
Width = SystemParameters.PrimaryScreenWidth;
}
private void InitializeCredentialsDialog(CredentialsDialogPurpose purpose, string message, string title)
{
InitializeBounds();
if (purpose == CredentialsDialogPurpose.WirelessNetwork)
{
PurposeIcon.Icon = FontAwesomeIcon.Wifi;
PurposeIcon.Rotation = 0;
UsernameLabel.Content = text.Get(TextKey.CredentialsDialog_UsernameOptionalLabel);
}
else
{
PurposeIcon.Icon = FontAwesomeIcon.Key;
PurposeIcon.Rotation = 90;
UsernameLabel.Content = text.Get(TextKey.CredentialsDialog_UsernameLabel);
}
PasswordLabel.Content = text.Get(TextKey.CredentialsDialog_PasswordLabel);
Message.Text = message;
Title = title;
Closed += (o, args) => closed?.Invoke();
Closing += (o, args) => closing?.Invoke();
Loaded += (o, args) => Activate();
CancelButton.Content = text.Get(TextKey.PasswordDialog_Cancel);
CancelButton.Click += CancelButton_Click;
ConfirmButton.Content = text.Get(TextKey.PasswordDialog_Confirm);
ConfirmButton.Click += ConfirmButton_Click;
Password.KeyDown += Password_KeyDown;
SystemParameters.StaticPropertyChanged += SystemParameters_StaticPropertyChanged;
}
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_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
DialogResult = true;
Close();
}
}
private void SystemParameters_StaticPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(SystemParameters.WorkArea))
{
Dispatcher.InvokeAsync(InitializeBounds);
}
}
}
}

View File

@@ -0,0 +1,61 @@
<Window x:Class="SafeExamBrowser.UserInterface.Mobile.Windows.ExamSelectionDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fa="http://schemas.fontawesome.io/icons/"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile.Windows"
mc:Ignorable="d" Background="Transparent" Height="750" Width="1000" FontSize="16" ResizeMode="NoResize" Topmost="True" AllowsTransparency="True" WindowStyle="None">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Templates/Colors.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid Background="#66000000">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" />
<Grid Grid.Row="1" Background="White">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<fa:ImageAwesome Grid.Column="0" Foreground="LightGray" Icon="PencilSquareOutline" Margin="50" Width="75" />
<Grid Grid.Column="1" Margin="0,0,50,50">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" x:Name="Message" Margin="0,25" TextWrapping="WrapWithOverflow" VerticalAlignment="Bottom" />
<ListBox Grid.Row="1" x:Name="ExamList" Cursor="Hand">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock FontWeight="Bold" Text="{Binding Name}" />
<StackPanel Orientation="Horizontal">
<TextBlock Margin="0,0,5,0" Text="{Binding Id}" />
<TextBlock Margin="0,0,5,0" Text="-" />
<TextBlock FontStyle="Italic" Text="{Binding LmsName}" />
</StackPanel>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Grid>
</Grid>
<Grid Grid.Row="2" Background="{StaticResource BackgroundBrush}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<WrapPanel Orientation="Horizontal" Margin="50,25" HorizontalAlignment="Right" VerticalAlignment="Center">
<Button x:Name="SelectButton" Cursor="Hand" Margin="10,0" Padding="10,5" MinWidth="75" IsEnabled="False" />
<Button x:Name="CancelButton" Cursor="Hand" Padding="10,5" MinWidth="75" />
</WrapPanel>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,111 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* 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.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Server.Contracts.Data;
using SafeExamBrowser.UserInterface.Contracts.Windows;
using SafeExamBrowser.UserInterface.Contracts.Windows.Data;
namespace SafeExamBrowser.UserInterface.Mobile.Windows
{
public partial class ExamSelectionDialog : Window, IExamSelectionDialog
{
private readonly IText text;
public ExamSelectionDialog(IEnumerable<Exam> exams, IText text)
{
this.text = text;
InitializeComponent();
InitializeExamSelectionDialog(exams);
}
public ExamSelectionDialogResult Show(IWindow parent = null)
{
return Dispatcher.Invoke(() =>
{
var result = new ExamSelectionDialogResult { Success = false };
if (parent is Window)
{
Owner = parent as Window;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
}
InitializeBounds();
if (ShowDialog() is true)
{
result.SelectedExam = ExamList.SelectedItem as Exam;
result.Success = true;
}
return result;
});
}
private void InitializeBounds()
{
Left = 0;
Top = 0;
Height = SystemParameters.PrimaryScreenHeight;
Width = SystemParameters.PrimaryScreenWidth;
}
private void InitializeExamSelectionDialog(IEnumerable<Exam> exams)
{
InitializeBounds();
Message.Text = text.Get(TextKey.ExamSelectionDialog_Message);
Title = text.Get(TextKey.ExamSelectionDialog_Title);
WindowStartupLocation = WindowStartupLocation.CenterScreen;
CancelButton.Content = text.Get(TextKey.ExamSelectionDialog_Cancel);
CancelButton.Click += CancelButton_Click;
SelectButton.Content = text.Get(TextKey.ExamSelectionDialog_Select);
SelectButton.Click += ConfirmButton_Click;
ExamList.ItemsSource = exams;
ExamList.SelectionChanged += ExamList_SelectionChanged;
Loaded += (o, args) => Activate();
SystemParameters.StaticPropertyChanged += SystemParameters_StaticPropertyChanged;
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
private void ConfirmButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = true;
Close();
}
private void ExamList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
SelectButton.IsEnabled = ExamList.SelectedItem != null;
}
private void SystemParameters_StaticPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(SystemParameters.WorkArea))
{
Dispatcher.InvokeAsync(InitializeBounds);
}
}
}
}

View File

@@ -0,0 +1,49 @@
<Window x:Class="SafeExamBrowser.UserInterface.Mobile.Windows.FileSystemDialog" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fa="http://schemas.fontawesome.io/icons/"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile"
mc:Ignorable="d" Height="750" Width="750" FontSize="16" ResizeMode="NoResize" Topmost="True" WindowState="Maximized">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Templates/Colors.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="25,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<fa:ImageAwesome Grid.Column="0" Name="OperationIcon" Foreground="LightGray" Height="35" Icon="FileOutline" />
<TextBlock Grid.Column="1" Name="Message" Margin="10,0,0,0" TextWrapping="Wrap" VerticalAlignment="Center" />
</Grid>
<TreeView Grid.Row="1" Name="FileSystem" Margin="10,0" TreeViewItem.Expanded="FileSystem_Expanded" />
<TextBlock Grid.Row="2" Name="SelectedElement" Margin="10,5,10,5" Padding="0,8" TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
<Grid Grid.Row="3" Name="NewElement" Margin="10,0,10,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Name="NewElementLabel" VerticalAlignment="Center"/>
<TextBox Grid.Column="1" Name="NewElementName" Margin="5,0,0,0" Padding="8" VerticalContentAlignment="Center" />
</Grid>
<Grid Grid.Row="4" Background="{StaticResource BackgroundBrush}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<WrapPanel Orientation="Horizontal" Margin="25" HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Name="SelectButton" Cursor="Hand" IsEnabled="False" Margin="20,0" Padding="20,10" MinWidth="75" />
<Button Name="CancelButton" Cursor="Hand" Padding="20,10" MinWidth="75" />
</WrapPanel>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,420 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* 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.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using FontAwesome.WPF;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.SystemComponents.Contracts;
using SafeExamBrowser.UserInterface.Contracts.FileSystemDialog;
using SafeExamBrowser.UserInterface.Contracts.Windows;
using SafeExamBrowser.UserInterface.Shared.Utilities;
namespace SafeExamBrowser.UserInterface.Mobile.Windows
{
internal partial class FileSystemDialog : Window
{
private readonly FileSystemElement element;
private readonly string initialPath;
private readonly string message;
private readonly FileSystemOperation operation;
private readonly IWindow parent;
private readonly ISystemInfo systemInfo;
private readonly bool restrictNavigation;
private readonly bool showElementPath;
private readonly IText text;
private readonly string title;
internal FileSystemDialog(
FileSystemElement element,
FileSystemOperation operation,
ISystemInfo systemInfo,
IText text,
string initialPath = default,
string message = default,
string title = default,
IWindow parent = default,
bool restrictNavigation = false,
bool showElementPath = true)
{
this.element = element;
this.initialPath = initialPath;
this.message = message;
this.operation = operation;
this.systemInfo = systemInfo;
this.parent = parent;
this.restrictNavigation = restrictNavigation;
this.showElementPath = showElementPath;
this.text = text;
this.title = title;
InitializeComponent();
InitializeDialog();
}
internal new FileSystemDialogResult Show()
{
var result = new FileSystemDialogResult();
if (parent is Window)
{
Owner = parent as Window;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
}
if (ShowDialog() == true)
{
result.FullPath = BuildFullPath();
result.Success = true;
}
return result;
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
private void FileSystem_Expanded(object sender, RoutedEventArgs e)
{
if (e.Source is TreeViewItem item && item.Items.Count == 1 && !(item.Items[0] is TreeViewItem))
{
Load(item);
}
}
private void FileSystem_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
if (e.NewValue is TreeViewItem item)
{
if (item.Tag is DirectoryInfo directory)
{
SelectButton.IsEnabled = element == FileSystemElement.Folder || operation == FileSystemOperation.Save;
SelectedElement.Text = directory.FullName;
SelectedElement.ToolTip = directory.FullName;
}
else if (item.Tag is FileInfo file)
{
SelectButton.IsEnabled = element == FileSystemElement.File;
SelectedElement.Text = file.FullName;
SelectedElement.ToolTip = file.FullName;
}
else
{
SelectButton.IsEnabled = false;
}
}
}
private void NewElementName_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter && IsValid())
{
DialogResult = true;
Close();
}
}
private void SelectButton_Click(object sender, RoutedEventArgs e)
{
if (IsValid())
{
DialogResult = true;
Close();
}
}
private string BuildFullPath()
{
var fullPath = SelectedElement.Text;
if (operation == FileSystemOperation.Save)
{
fullPath = Path.Combine(SelectedElement.Text, NewElementName.Text);
if (element == FileSystemElement.File)
{
var extension = Path.GetExtension(initialPath);
if (!fullPath.EndsWith(extension))
{
fullPath = $"{fullPath}{extension}";
}
}
}
return fullPath;
}
private bool IsValid()
{
var fullPath = BuildFullPath();
var isValid = true;
if (element == FileSystemElement.File && operation == FileSystemOperation.Save && File.Exists(fullPath))
{
var message = text.Get(TextKey.FileSystemDialog_OverwriteWarning);
var title = text.Get(TextKey.FileSystemDialog_OverwriteWarningTitle);
var result = System.Windows.MessageBox.Show(this, message, title, MessageBoxButton.YesNo, MessageBoxImage.Warning);
isValid = result == MessageBoxResult.Yes;
}
return isValid;
}
private void Load(TreeViewItem item)
{
item.Items.Clear();
if (item.Tag is DirectoryInfo directory)
{
item.Cursor = Cursors.Wait;
item.BeginInit();
try
{
foreach (var subDirectory in directory.GetDirectories())
{
if (!subDirectory.Attributes.HasFlag(FileAttributes.Hidden) || initialPath?.Contains(subDirectory.FullName) == true)
{
item.Items.Add(CreateItem(subDirectory));
}
}
foreach (var file in directory.GetFiles())
{
if (!file.Attributes.HasFlag(FileAttributes.Hidden) || initialPath?.Contains(file.FullName) == true)
{
item.Items.Add(CreateItem(file));
}
}
}
catch (Exception e)
{
item.Items.Add(CreateErrorItem(e));
}
item.EndInit();
item.Cursor = Cursors.Hand;
}
}
private TreeViewItem CreateErrorItem(Exception e)
{
var item = new TreeViewItem();
item.Foreground = Brushes.Red;
item.Header = $"{text.Get(TextKey.FileSystemDialog_LoadError)} {e.Message}";
item.ToolTip = e.GetType() + Environment.NewLine + e.StackTrace;
return item;
}
private TreeViewItem CreateItem(DirectoryInfo directory)
{
var header = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(2) };
var image = new Image
{
Height = 24,
Source = IconLoader.LoadIconFor(directory)
};
var item = new TreeViewItem();
var textBlock = new TextBlock { Margin = new Thickness(5, 0, 0, 0), Text = directory.Name, VerticalAlignment = VerticalAlignment.Center };
header.Children.Add(image);
header.Children.Add(textBlock);
item.Cursor = Cursors.Hand;
item.Header = header;
item.Tag = directory;
item.ToolTip = directory.FullName;
item.Items.Add(text.Get(TextKey.FileSystemDialog_Loading));
return item;
}
private TreeViewItem CreateItem(FileInfo file)
{
var header = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(2) };
var image = new Image
{
Height = 24,
Source = IconLoader.LoadIconFor(file)
};
var item = new TreeViewItem();
var textBlock = new TextBlock { Margin = new Thickness(5, 0, 0, 0), Text = file.Name, VerticalAlignment = VerticalAlignment.Center };
header.Children.Add(image);
header.Children.Add(textBlock);
item.Header = header;
item.Tag = file;
item.ToolTip = file.FullName;
if (element == FileSystemElement.File && operation == FileSystemOperation.Open)
{
item.Cursor = Cursors.Hand;
}
else
{
item.Cursor = Cursors.No;
item.Focusable = false;
textBlock.Foreground = Brushes.Gray;
}
return item;
}
private void InitializeDialog()
{
CancelButton.Click += CancelButton_Click;
CancelButton.Content = text.Get(TextKey.FileSystemDialog_Cancel);
FileSystem.SelectedItemChanged += FileSystem_SelectedItemChanged;
NewElement.Visibility = operation == FileSystemOperation.Save ? Visibility.Visible : Visibility.Collapsed;
NewElementLabel.Text = text.Get(TextKey.FileSystemDialog_SaveAs);
NewElementName.KeyUp += NewElementName_KeyUp;
OperationIcon.Icon = operation == FileSystemOperation.Save ? FontAwesomeIcon.Download : FontAwesomeIcon.Search;
SelectButton.Click += SelectButton_Click;
SelectButton.Content = text.Get(TextKey.FileSystemDialog_Select);
SelectedElement.Visibility = showElementPath ? Visibility.Visible : Visibility.Hidden;
InitializeText();
InitializeFileSystem();
}
private void InitializeFileSystem()
{
if (restrictNavigation && !string.IsNullOrEmpty(initialPath))
{
InitializeRestricted();
}
else
{
InitializeUnrestricted();
}
}
private void InitializeRestricted()
{
var root = Directory.Exists(initialPath) ? initialPath : Path.GetDirectoryName(initialPath);
if (Directory.Exists(root))
{
var directory = CreateItem(new DirectoryInfo(root));
FileSystem.Items.Add(directory);
directory.IsExpanded = true;
directory.IsSelected = true;
directory.BringIntoView();
}
}
private void InitializeUnrestricted()
{
foreach (var drive in systemInfo.GetDrives())
{
FileSystem.Items.Add(CreateItem(drive.RootDirectory));
}
if (FileSystem.HasItems && FileSystem.Items[0] is TreeViewItem item)
{
item.IsSelected = true;
}
if (!string.IsNullOrEmpty(initialPath))
{
var root = Path.GetPathRoot(initialPath);
var path = initialPath.Replace(root, "").Split(Path.DirectorySeparatorChar);
var segments = new List<string> { root };
segments.AddRange(path);
SelectInitialPath(FileSystem.Items, segments);
if (element == FileSystemElement.File && operation == FileSystemOperation.Save)
{
NewElementName.Text = Path.GetFileName(initialPath);
}
}
}
private void SelectInitialPath(ItemCollection items, List<string> segments)
{
var segment = segments.FirstOrDefault();
if (segment != default)
{
foreach (var item in items)
{
if (item is TreeViewItem i && i.Tag is DirectoryInfo d && d.Name.Equals(segment))
{
i.IsExpanded = true;
i.IsSelected = true;
i.BringIntoView();
SelectInitialPath(i.Items, segments.Skip(1).ToList());
break;
}
}
}
}
private void InitializeText()
{
if (string.IsNullOrEmpty(message))
{
if (element == FileSystemElement.File)
{
if (operation == FileSystemOperation.Open)
{
Message.Text = text.Get(TextKey.FileSystemDialog_OpenFileMessage);
}
else
{
Message.Text = text.Get(TextKey.FileSystemDialog_SaveFileMessage);
}
}
else
{
if (operation == FileSystemOperation.Open)
{
Message.Text = text.Get(TextKey.FileSystemDialog_OpenFolderMessage);
}
else
{
Message.Text = text.Get(TextKey.FileSystemDialog_SaveFolderMessage);
}
}
}
else
{
Message.Text = message;
}
if (string.IsNullOrEmpty(title))
{
Title = text.Get(TextKey.FileSystemDialog_Title);
}
else
{
Title = title;
}
}
}
}

View File

@@ -0,0 +1,34 @@
<Window x:Class="SafeExamBrowser.UserInterface.Mobile.Windows.LockScreen" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile"
mc:Ignorable="d" d:DesignWidth="1500" Background="Red" FontSize="16" ResizeMode="NoResize" Topmost="True" WindowState="Maximized" WindowStyle="None">
<Grid FocusManager.FocusedElement="{Binding ElementName=Password}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="1" Orientation="Vertical">
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal" Margin="0,10">
<Image Height="100" Margin="0,0,20,0" Source="../Images/SafeExamBrowser.ico" />
<TextBlock Name="Heading" Foreground="White" FontSize="75" FontWeight="ExtraBold" TextAlignment="Center" Text="SEB LOCKED" />
</StackPanel>
<TextBlock Name="Message" Foreground="White" FontSize="24" FontWeight="DemiBold" Margin="0,10" Padding="5" TextWrapping="Wrap" />
<StackPanel Name="Options" Margin="0,10" Orientation="Vertical" />
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal" Margin="0,10">
<PasswordBox Name="Password" Margin="10,5" MinWidth="500" Padding="12" />
<Button Name="Button" Cursor="Hand" Margin="10,5" MinWidth="125" Padding="12" />
</StackPanel>
</StackPanel>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,214 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* 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.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Settings.UserInterface;
using SafeExamBrowser.UserInterface.Contracts.Windows;
using SafeExamBrowser.UserInterface.Contracts.Windows.Data;
using SafeExamBrowser.UserInterface.Contracts.Windows.Events;
using SafeExamBrowser.UserInterface.Shared.Utilities;
using Screen = System.Windows.Forms.Screen;
namespace SafeExamBrowser.UserInterface.Mobile.Windows
{
internal partial class LockScreen : Window, ILockScreen
{
private readonly AutoResetEvent autoResetEvent;
private readonly LockScreenSettings settings;
private readonly IText text;
private bool canceled;
private IList<Window> windows;
event WindowClosedEventHandler IWindow.Closed
{
add { throw new NotImplementedException(); }
remove { throw new NotImplementedException(); }
}
event WindowClosingEventHandler IWindow.Closing
{
add { throw new NotImplementedException(); }
remove { throw new NotImplementedException(); }
}
internal LockScreen(string message, string title, LockScreenSettings settings, IText text, IEnumerable<LockScreenOption> options)
{
this.autoResetEvent = new AutoResetEvent(false);
this.settings = settings;
this.text = text;
InitializeComponent();
InitializeLockScreen(message, title, options);
}
public void BringToForeground()
{
Dispatcher.Invoke(Activate);
}
public void Cancel()
{
canceled = true;
autoResetEvent.Set();
}
public new void Close()
{
Dispatcher.Invoke(CloseAll);
}
public void InitializeBounds()
{
Dispatcher.Invoke(() =>
{
foreach (var window in windows)
{
window.Topmost = false;
window.WindowState = WindowState.Normal;
window.Activate();
window.Topmost = true;
window.WindowState = WindowState.Maximized;
}
Topmost = false;
WindowState = WindowState.Normal;
Activate();
Topmost = true;
WindowState = WindowState.Maximized;
});
}
public new void Show()
{
Dispatcher.Invoke(ShowAll);
}
public LockScreenResult WaitForResult()
{
var result = new LockScreenResult();
autoResetEvent.WaitOne();
Dispatcher.Invoke(() =>
{
result.Password = Password.Password;
foreach (var child in Options.Children)
{
if (child is RadioButton option && option.IsChecked == true)
{
result.OptionId = option.Tag as Guid?;
}
}
});
result.Canceled = canceled;
return result;
}
private void InitializeLockScreen(string message, string title, IEnumerable<LockScreenOption> options)
{
windows = new List<Window>();
Button.Content = text.Get(TextKey.LockScreen_UnlockButton);
Button.Click += Button_Click;
Heading.Text = title;
Loaded += (o, args) => Activate();
Message.Text = message;
Password.KeyDown += Password_KeyDown;
if (Parser.TryParseBrush(settings.BackgroundColor, out var brush))
{
Background = brush;
}
foreach (var option in options)
{
Options.Children.Add(new RadioButton
{
Content = new TextBlock { Text = option.Text, TextWrapping = TextWrapping.Wrap },
Cursor = Cursors.Hand,
FontSize = Message.FontSize,
Foreground = Message.Foreground,
IsChecked = options.First() == option,
Margin = new Thickness(5, options.First() == option ? 5 : 10, 5, options.Last() == option ? 5 : 10),
Tag = option.Id,
VerticalContentAlignment = VerticalAlignment.Center
});
}
}
private void CloseAll()
{
foreach (var window in windows)
{
window.Close();
}
base.Close();
}
private void ShowAll()
{
foreach (var screen in Screen.AllScreens)
{
if (!screen.Primary)
{
ShowLockWindowOn(screen);
}
}
base.Show();
}
private void ShowLockWindowOn(Screen screen)
{
var window = new Window();
var position = this.TransformFromPhysical(screen.Bounds.X, screen.Bounds.Y);
var size = this.TransformFromPhysical(screen.Bounds.Width, screen.Bounds.Height);
window.Background = Brushes.Red;
window.Topmost = true;
window.Left = position.X;
window.Top = position.Y;
window.Width = size.X;
window.Height = size.Y;
window.ResizeMode = ResizeMode.NoResize;
window.WindowStyle = WindowStyle.None;
window.Show();
windows.Add(window);
// The window can only be maximized after it was shown on its screen, otherwise it is rendered on the primary screen!
window.WindowState = WindowState.Maximized;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
autoResetEvent.Set();
}
private void Password_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
autoResetEvent.Set();
}
}
}
}

View File

@@ -0,0 +1,30 @@
<Window x:Class="SafeExamBrowser.UserInterface.Mobile.Windows.LogWindow" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile"
mc:Ignorable="d" Title="{Binding Path=WindowTitle}" Background="Black" Foreground="White" Height="750" Width="1000" MinHeight="350"
MinWidth="350" Topmost="True" WindowState="Maximized" WindowStartupLocation="CenterScreen" Icon="../Images/LogNotification.ico">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Templates/ScrollViewers.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Background="White" Orientation="Horizontal">
<CheckBox Cursor="Hand" IsChecked="{Binding AutoScroll}" Margin="5" VerticalContentAlignment="Center" Content="{Binding AutoScrollTitle}" />
<CheckBox Cursor="Hand" IsChecked="{Binding Path=Topmost, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}" Margin="5" VerticalContentAlignment="Center" Content="{Binding TopmostTitle}" />
</StackPanel>
<ScrollViewer x:Name="ScrollViewer" Grid.Row="1" HorizontalScrollBarVisibility="Auto" Padding="5,5,5,0" PanningMode="Both"
VerticalScrollBarVisibility="Auto" Template="{StaticResource SmallBarScrollViewer}">
<TextBlock x:Name="LogContent" FontFamily="Consolas" Foreground="White" />
</ScrollViewer>
</Grid>
</Window>

View File

@@ -0,0 +1,111 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* 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 System.Windows.Input;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Windows;
using SafeExamBrowser.UserInterface.Contracts.Windows.Events;
using SafeExamBrowser.UserInterface.Mobile.ViewModels;
namespace SafeExamBrowser.UserInterface.Mobile.Windows
{
internal partial class LogWindow : Window, IWindow
{
private readonly ILogger logger;
private readonly LogViewModel model;
private WindowClosedEventHandler closed;
private WindowClosingEventHandler closing;
event WindowClosedEventHandler IWindow.Closed
{
add { closed += value; }
remove { closed -= value; }
}
event WindowClosingEventHandler IWindow.Closing
{
add { closing += value; }
remove { closing -= value; }
}
internal 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;
Closed += (o, args) => closed?.Invoke();
Closing += LogWindow_Closing;
Loaded += LogWindow_Loaded;
}
private void LogWindow_Loaded(object sender, RoutedEventArgs e)
{
Cursor = Cursors.Wait;
var log = logger.GetLog();
foreach (var content in log)
{
model.Notify(content);
}
logger.Subscribe(model);
logger.Debug("Opened log window.");
Cursor = Cursors.Arrow;
}
private void LogWindow_Closing(object sender, CancelEventArgs e)
{
logger.Unsubscribe(model);
logger.Debug("Closed log window.");
closing?.Invoke();
}
}
}

View File

@@ -0,0 +1,43 @@
<Window x:Class="SafeExamBrowser.UserInterface.Mobile.Windows.MessageBoxDialog" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fa="http://schemas.fontawesome.io/icons/"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile"
mc:Ignorable="d" Background="Transparent" Height="750" Width="1000" FontSize="16" ResizeMode="NoResize" Topmost="True" AllowsTransparency="True" WindowStyle="None">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Templates/Colors.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid Background="#66000000">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" />
<Grid Grid.Row="1" Background="White">
<Grid Margin="25">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ContentControl Grid.Column="0" x:Name="Image" Margin="25" Width="75" />
<TextBlock Grid.Column="1" x:Name="Message" Margin="12,20" TextWrapping="WrapWithOverflow" VerticalAlignment="Center" />
</Grid>
</Grid>
<Grid Grid.Row="2" Background="{StaticResource BackgroundBrush}">
<WrapPanel Orientation="Horizontal" Margin="50,25" HorizontalAlignment="Right" VerticalAlignment="Center">
<Button x:Name="OkButton" Cursor="Hand" Padding="20,10" MinWidth="100" />
<Button x:Name="CancelButton" Cursor="Hand" Margin="20,0,0,0" Padding="20,10" MinWidth="100" />
<Button x:Name="YesButton" Cursor="Hand" Margin="0,0,20,0" Padding="20,10" MinWidth="100" />
<Button x:Name="NoButton" Cursor="Hand" Padding="20,10" MinWidth="100" />
</WrapPanel>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,183 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* 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.Drawing;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.MessageBox;
using MessageBoxResult = SafeExamBrowser.UserInterface.Contracts.MessageBox.MessageBoxResult;
namespace SafeExamBrowser.UserInterface.Mobile.Windows
{
internal partial class MessageBoxDialog : Window
{
private readonly IText text;
public MessageBoxDialog(IText text)
{
this.text = text;
InitializeComponent();
InitializeMessageBox();
}
internal MessageBoxResult Show(string message, string title, MessageBoxAction action, MessageBoxIcon icon, Window parent = null)
{
Message.Text = message;
Title = title;
if (parent is Window)
{
Owner = parent as Window;
}
InitializeBounds();
InitializeAction(action);
InitializeIcon(icon);
ShowDialog();
return ResultFor(action);
}
private void InitializeAction(MessageBoxAction action)
{
switch (action)
{
case MessageBoxAction.Ok:
CancelButton.Visibility = Visibility.Collapsed;
OkButton.Visibility = Visibility.Visible;
OkButton.Focus();
YesButton.Visibility = Visibility.Collapsed;
NoButton.Visibility = Visibility.Collapsed;
break;
case MessageBoxAction.OkCancel:
CancelButton.Visibility = Visibility.Visible;
CancelButton.Focus();
OkButton.Visibility = Visibility.Visible;
YesButton.Visibility = Visibility.Collapsed;
NoButton.Visibility = Visibility.Collapsed;
break;
case MessageBoxAction.YesNo:
CancelButton.Visibility = Visibility.Collapsed;
OkButton.Visibility = Visibility.Collapsed;
YesButton.Visibility = Visibility.Visible;
NoButton.Visibility = Visibility.Visible;
NoButton.Focus();
break;
}
}
private void InitializeBounds()
{
Left = 0;
Top = 0;
Height = SystemParameters.PrimaryScreenHeight;
Width = SystemParameters.PrimaryScreenWidth;
}
private void InitializeIcon(MessageBoxIcon icon)
{
var handle = default(IntPtr);
switch (icon)
{
case MessageBoxIcon.Error:
handle = SystemIcons.Error.Handle;
break;
case MessageBoxIcon.Information:
handle = SystemIcons.Information.Handle;
break;
case MessageBoxIcon.Question:
handle = SystemIcons.Question.Handle;
break;
case MessageBoxIcon.Warning:
handle = SystemIcons.Warning.Handle;
break;
}
if (handle != default(IntPtr))
{
Image.Content = new System.Windows.Controls.Image
{
Source = Imaging.CreateBitmapSourceFromHIcon(handle, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions())
};
}
}
private void InitializeMessageBox()
{
InitializeBounds();
CancelButton.Content = text.Get(TextKey.MessageBox_CancelButton);
CancelButton.Click += CancelButton_Click;
NoButton.Content = text.Get(TextKey.MessageBox_NoButton);
NoButton.Click += NoButton_Click;
OkButton.Content = text.Get(TextKey.MessageBox_OkButton);
OkButton.Click += OkButton_Click;
YesButton.Content = text.Get(TextKey.MessageBox_YesButton);
YesButton.Click += YesButton_Click;
SystemParameters.StaticPropertyChanged += SystemParameters_StaticPropertyChanged;
}
private MessageBoxResult ResultFor(MessageBoxAction action)
{
switch (action)
{
case MessageBoxAction.Ok:
return DialogResult == true ? MessageBoxResult.Ok : MessageBoxResult.None;
case MessageBoxAction.OkCancel:
return DialogResult == true ? MessageBoxResult.Ok : MessageBoxResult.Cancel;
case MessageBoxAction.YesNo:
return DialogResult == true ? MessageBoxResult.Yes : MessageBoxResult.No;
default:
return MessageBoxResult.None;
}
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
private void NoButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
private void OkButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = true;
Close();
}
private void YesButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = true;
Close();
}
private void SystemParameters_StaticPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(SystemParameters.WorkArea))
{
Dispatcher.InvokeAsync(InitializeBounds);
}
}
}
}

View File

@@ -0,0 +1,48 @@
<Window x:Class="SafeExamBrowser.UserInterface.Mobile.Windows.PasswordDialog" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fa="http://schemas.fontawesome.io/icons/"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile"
mc:Ignorable="d" Background="Transparent" Height="750" Width="1000" FontSize="16" ResizeMode="NoResize" Topmost="True" AllowsTransparency="True" WindowStyle="None">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Templates/Colors.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid Background="#66000000" FocusManager.FocusedElement="{Binding ElementName=Password}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" />
<Grid Grid.Row="1" Background="White">
<Grid Margin="25">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<fa:ImageAwesome Grid.Column="0" Foreground="LightGray" Icon="Key" Margin="25" Rotation="90" Width="75" />
<Grid Grid.Column="1" Margin="25,0,25,25">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" x:Name="Message" Margin="0,20" TextWrapping="WrapWithOverflow" VerticalAlignment="Bottom" />
<PasswordBox Grid.Row="1" x:Name="Password" Padding="12" VerticalContentAlignment="Center" />
</Grid>
</Grid>
</Grid>
<Grid Grid.Row="2" Background="{StaticResource BackgroundBrush}">
<WrapPanel Orientation="Horizontal" Margin="50,25" HorizontalAlignment="Right" VerticalAlignment="Center">
<Button x:Name="ConfirmButton" Cursor="Hand" Margin="20,0" Padding="20,10" MinWidth="100" />
<Button x:Name="CancelButton" Cursor="Hand" Padding="20,10" MinWidth="100" />
</WrapPanel>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,132 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* 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.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Windows;
using SafeExamBrowser.UserInterface.Contracts.Windows.Data;
using SafeExamBrowser.UserInterface.Contracts.Windows.Events;
namespace SafeExamBrowser.UserInterface.Mobile.Windows
{
internal partial class PasswordDialog : Window, IPasswordDialog
{
private readonly IText text;
private WindowClosedEventHandler closed;
private WindowClosingEventHandler closing;
event WindowClosedEventHandler IWindow.Closed
{
add { closed += value; }
remove { closed -= value; }
}
event WindowClosingEventHandler IWindow.Closing
{
add { closing += value; }
remove { closing -= value; }
}
internal PasswordDialog(string message, string title, IText text)
{
this.text = text;
InitializeComponent();
InitializePasswordDialog(message, title);
}
public void BringToForeground()
{
Dispatcher.Invoke(Activate);
}
public PasswordDialogResult Show(IWindow parent = null)
{
return Dispatcher.Invoke(() =>
{
var result = new PasswordDialogResult { Success = false };
if (parent is Window)
{
Owner = parent as Window;
}
InitializeBounds();
if (ShowDialog() is true)
{
result.Password = Password.Password;
result.Success = true;
}
return result;
});
}
private void InitializeBounds()
{
Left = 0;
Top = 0;
Height = SystemParameters.PrimaryScreenHeight;
Width = SystemParameters.PrimaryScreenWidth;
}
private void InitializePasswordDialog(string message, string title)
{
InitializeBounds();
Message.Text = message;
Title = title;
Closed += (o, args) => closed?.Invoke();
Closing += (o, args) => closing?.Invoke();
Loaded += (o, args) => Activate();
CancelButton.Content = text.Get(TextKey.PasswordDialog_Cancel);
CancelButton.Click += CancelButton_Click;
ConfirmButton.Content = text.Get(TextKey.PasswordDialog_Confirm);
ConfirmButton.Click += ConfirmButton_Click;
Password.KeyDown += Password_KeyDown;
SystemParameters.StaticPropertyChanged += SystemParameters_StaticPropertyChanged;
}
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_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
DialogResult = true;
Close();
}
}
private void SystemParameters_StaticPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(SystemParameters.WorkArea))
{
Dispatcher.InvokeAsync(InitializeBounds);
}
}
}
}

View File

@@ -0,0 +1,56 @@
<Window x:Class="SafeExamBrowser.UserInterface.Mobile.Windows.ProctoringFinalizationDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fa="http://schemas.fontawesome.io/icons/"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile.Windows"
mc:Ignorable="d" Background="Transparent" Height="750" Width="1000" FontSize="16" ResizeMode="NoResize" Topmost="True" AllowsTransparency="True" WindowStyle="None">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Templates/Colors.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid Background="#66000000">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" />
<Grid Grid.Row="1" Background="White">
<Grid Name="ProgressPanel" Margin="50">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Name="Info" TextWrapping="WrapWithOverflow" VerticalAlignment="Bottom" />
<Grid Grid.Row="1">
<ProgressBar Name="Progress" Height="35" Margin="0,25" />
<TextBlock Name="Percentage" FontWeight="Bold" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
<TextBlock Grid.Row="2" Name="Status" FontStyle="Italic" TextWrapping="WrapWithOverflow" VerticalAlignment="Top" />
</Grid>
<Grid Name="FailurePanel" Margin="50">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<fa:ImageAwesome Grid.Column="0" Foreground="LightGray" Icon="Warning" Margin="10,0,50,0" Width="50" />
<WrapPanel Grid.Column="1" Orientation="Vertical" VerticalAlignment="Center">
<TextBlock Name="Message" TextWrapping="WrapWithOverflow" />
<TextBlock Name="CachePath" FontFamily="Courier New" Margin="0,25,0,0" TextWrapping="WrapWithOverflow" />
</WrapPanel>
</Grid>
</Grid>
<Grid Grid.Row="2" Name="ButtonPanel" Background="{StaticResource BackgroundBrush}">
<WrapPanel Orientation="Horizontal" Margin="50,25" HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Name="Button" Cursor="Hand" Padding="10,5" MinWidth="75" />
</WrapPanel>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,139 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* 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 System.Windows.Input;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Proctoring.Contracts.Events;
using SafeExamBrowser.UserInterface.Contracts.Proctoring;
namespace SafeExamBrowser.UserInterface.Mobile.Windows
{
public partial class ProctoringFinalizationDialog : Window, IProctoringFinalizationDialog
{
private readonly IText text;
public ProctoringFinalizationDialog(IText text)
{
this.text = text;
InitializeComponent();
InitializeDialog();
}
public new void Show()
{
Dispatcher.Invoke(() =>
{
InitializeBounds();
ShowDialog();
});
}
public void Update(RemainingWorkUpdatedEventArgs status)
{
Dispatcher.Invoke(() =>
{
if (status.HasFailed)
{
ShowFailure(status);
}
else if (status.IsFinished)
{
Close();
}
else
{
ShowProgress(status);
}
});
}
private void InitializeBounds()
{
Left = 0;
Top = 0;
Height = SystemParameters.PrimaryScreenHeight;
Width = SystemParameters.PrimaryScreenWidth;
}
private void InitializeDialog()
{
Button.Click += (o, args) => Close();
Button.Content = text.Get(TextKey.ProctoringFinalizationDialog_Confirm);
Title = text.Get(TextKey.ProctoringFinalizationDialog_Title);
InitializeBounds();
SystemParameters.StaticPropertyChanged += SystemParameters_StaticPropertyChanged;
}
private void ShowFailure(RemainingWorkUpdatedEventArgs status)
{
ButtonPanel.Visibility = Visibility.Visible;
// TODO: Revert once cache handling has been specified and changed!
CachePath.Text = status.CachePath ?? "-";
CachePath.Visibility = Visibility.Collapsed;
Cursor = Cursors.Arrow;
FailurePanel.Visibility = Visibility.Visible;
Message.Text = text.Get(TextKey.ProctoringFinalizationDialog_FailureMessage);
ProgressPanel.Visibility = Visibility.Collapsed;
}
private void ShowProgress(RemainingWorkUpdatedEventArgs status)
{
ButtonPanel.Visibility = Visibility.Collapsed;
Cursor = Cursors.Wait;
FailurePanel.Visibility = Visibility.Collapsed;
Info.Text = text.Get(TextKey.ProctoringFinalizationDialog_InfoMessage);
ProgressPanel.Visibility = Visibility.Visible;
if (status.IsWaiting)
{
var count = $"{status.Total - status.Progress}";
var time = $"{status.Resume.ToLongTimeString()}";
Percentage.Text = "";
Progress.IsIndeterminate = true;
Status.Text = text.Get(TextKey.ProctoringFinalizationDialog_StatusWaiting).Replace("%%_COUNT_%%", count).Replace("%%_TIME_%%", time);
}
else
{
var count = $"{status.Progress}";
var total = $"{status.Total}";
Percentage.Text = $"{status.Progress / (double) (status.Total > 0 ? status.Total : 1) * 100:N0}%";
Progress.IsIndeterminate = false;
Progress.Maximum = status.Total;
Progress.Value = status.Progress;
if (status.Next.HasValue)
{
Status.Text = text.Get(TextKey.ProctoringFinalizationDialog_StatusAndTime).Replace("%%_TIME_%%", $"{status.Next.Value.ToLongTimeString()}");
}
else
{
Status.Text = text.Get(TextKey.ProctoringFinalizationDialog_Status);
}
Status.Text = Status.Text.Replace("%%_COUNT_%%", count).Replace("%%_TOTAL_%%", total);
}
}
private void SystemParameters_StaticPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(SystemParameters.WorkArea))
{
Dispatcher.InvokeAsync(InitializeBounds);
}
}
}
}

View File

@@ -0,0 +1,8 @@
<Window x:Class="SafeExamBrowser.UserInterface.Mobile.Windows.ProctoringWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile.Windows"
mc:Ignorable="d" Height="550" Width="650" MinHeight="250" MinWidth="250" Topmost="True" WindowStyle="ToolWindow">
</Window>

View File

@@ -0,0 +1,140 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* 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.Threading.Tasks;
using System.Windows;
using SafeExamBrowser.UserInterface.Contracts.Proctoring;
using SafeExamBrowser.UserInterface.Contracts.Windows;
using SafeExamBrowser.UserInterface.Contracts.Windows.Events;
using SafeExamBrowser.UserInterface.Shared.Utilities;
namespace SafeExamBrowser.UserInterface.Mobile.Windows
{
public partial class ProctoringWindow : Window, IProctoringWindow
{
private WindowClosedEventHandler closed;
private WindowClosingEventHandler closing;
event WindowClosedEventHandler IWindow.Closed
{
add { closed += value; }
remove { closed -= value; }
}
event WindowClosingEventHandler IWindow.Closing
{
add { closing += value; }
remove { closing -= value; }
}
public ProctoringWindow(IProctoringControl control)
{
InitializeComponent();
InitializeWindow(control);
}
public void BringToForeground()
{
Dispatcher.Invoke(() =>
{
if (WindowState == WindowState.Minimized)
{
WindowState = WindowState.Normal;
}
Activate();
});
}
public new void Close()
{
Dispatcher.Invoke(() =>
{
Closing -= ProctoringWindow_Closing;
closing?.Invoke();
base.Close();
});
}
public new void Hide()
{
Dispatcher.Invoke(base.Hide);
}
public void HideWithDelay()
{
Dispatcher.Invoke(() => this.MoveToBackground());
Task.Delay(15000).ContinueWith(_ => Hide());
}
public void SetTitle(string title)
{
Dispatcher.Invoke(() => Title = title ?? "");
}
public new void Show()
{
Dispatcher.Invoke(base.Show);
}
public void Toggle()
{
Dispatcher.Invoke(() =>
{
if (Visibility == Visibility.Visible)
{
base.Hide();
}
else
{
base.Show();
}
});
}
private void InitializeWindow(IProctoringControl control)
{
if (control is UIElement element)
{
Content = element;
control.FullScreenChanged += Control_FullScreenChanged;
}
Closed += (o, args) => closed?.Invoke();
Closing += ProctoringWindow_Closing;
Loaded += ProctoringWindow_Loaded;
Top = SystemParameters.WorkArea.Height - Height - 15;
Left = SystemParameters.WorkArea.Width - Width - 20;
}
private void Control_FullScreenChanged(bool fullScreen)
{
if (fullScreen)
{
WindowState = WindowState.Maximized;
WindowStyle = WindowStyle.None;
}
else
{
WindowState = WindowState.Normal;
WindowStyle = WindowStyle.ToolWindow;
}
}
private void ProctoringWindow_Closing(object sender, CancelEventArgs e)
{
e.Cancel = true;
}
private void ProctoringWindow_Loaded(object sender, RoutedEventArgs e)
{
this.HideCloseButton();
}
}
}

View File

@@ -0,0 +1,47 @@
<Window x:Class="SafeExamBrowser.UserInterface.Mobile.Windows.RuntimeWindow" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile"
xmlns:s="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d" Background="White" Foreground="Black" Height="500" Width="850" WindowStyle="None" WindowStartupLocation="CenterScreen"
Icon="../Images/SafeExamBrowser.ico" ResizeMode="NoResize" Title="Safe Exam Browser" Topmost="True">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Templates/ScrollViewers.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Border BorderBrush="DodgerBlue" BorderThickness="1">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<RowDefinition Height="25" />
<RowDefinition Height="3*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="400" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Grid.ColumnSpan="2" Margin="-25,0,0,0" Source="pack://application:,,,/SafeExamBrowser.UserInterface.Mobile;component/Images/SplashScreen.png" />
<TextBlock x:Name="InfoTextBlock" Grid.Column="1" FontSize="10" Foreground="DimGray" Margin="10,70,225,10" TextWrapping="Wrap" />
</Grid>
<ProgressBar x:Name="ProgressBar" Grid.Row="1" Background="WhiteSmoke" BorderThickness="0" Foreground="DodgerBlue"
IsIndeterminate="{Binding IsIndeterminate}" Maximum="{Binding MaxProgress}" Value="{Binding CurrentProgress}"
Visibility="{Binding ProgressBarVisibility}" />
<TextBlock x:Name="StatusTextBlock" Grid.Row="1" FontSize="12" FontWeight="Bold" HorizontalAlignment="Center"
Text="{Binding Status}" VerticalAlignment="Center" />
<Border Grid.Row="2" BorderBrush="DodgerBlue" BorderThickness="0,0.5,0,0">
<ScrollViewer x:Name="LogScrollViewer" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Margin="0,10,0,0"
Template="{StaticResource SmallBarScrollViewer}" Visibility="Collapsed">
<TextBlock x:Name="LogTextBlock" Background="Transparent" FontFamily="Consolas" FontSize="10" />
</ScrollViewer>
</Border>
</Grid>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,159 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* 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 System.Windows.Media;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Windows;
using SafeExamBrowser.UserInterface.Contracts.Windows.Events;
using SafeExamBrowser.UserInterface.Mobile.ViewModels;
namespace SafeExamBrowser.UserInterface.Mobile.Windows
{
internal partial class RuntimeWindow : Window, IRuntimeWindow
{
private readonly AppConfig appConfig;
private readonly IText text;
private bool allowClose;
private RuntimeWindowViewModel model;
private WindowClosedEventHandler closed;
private WindowClosingEventHandler closing;
public bool ShowLog
{
set => Dispatcher.Invoke(() => LogScrollViewer.Visibility = value ? Visibility.Visible : Visibility.Collapsed);
}
public bool ShowProgressBar
{
set => Dispatcher.Invoke(() => model.ProgressBarVisibility = value ? Visibility.Visible : Visibility.Hidden);
}
public bool TopMost
{
set => Dispatcher.Invoke(() => Topmost = value);
}
event WindowClosedEventHandler IWindow.Closed
{
add { closed += value; }
remove { closed -= value; }
}
event WindowClosingEventHandler IWindow.Closing
{
add { closing += value; }
remove { closing -= value; }
}
internal 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;
closing?.Invoke();
base.Close();
});
}
public new void Hide()
{
Dispatcher.Invoke(base.Hide);
}
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.IsIndeterminate = false;
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;
}
public new void Show()
{
Dispatcher.Invoke(base.Show);
}
private void InitializeRuntimeWindow()
{
Title = $"{appConfig.ProgramTitle} - Version {appConfig.ProgramInformationalVersion}";
InfoTextBlock.Inlines.Add(new Run($"Version {appConfig.ProgramInformationalVersion}") { FontSize = 12 });
InfoTextBlock.Inlines.Add(new LineBreak());
InfoTextBlock.Inlines.Add(new Run($"Build {appConfig.ProgramBuildVersion}") { FontSize = 8, Foreground = Brushes.Gray });
InfoTextBlock.Inlines.Add(new LineBreak());
InfoTextBlock.Inlines.Add(new LineBreak());
InfoTextBlock.Inlines.Add(new Run(appConfig.ProgramCopyright) { FontSize = 10, Foreground = Brushes.Gray });
model = new RuntimeWindowViewModel(LogTextBlock);
ProgressBar.DataContext = model;
StatusTextBlock.DataContext = model;
Closed += (o, args) => closed?.Invoke();
Closing += (o, args) => args.Cancel = !allowClose;
#if DEBUG
Topmost = false;
#endif
}
}
}

View File

@@ -0,0 +1,45 @@
<Window x:Class="SafeExamBrowser.UserInterface.Mobile.Windows.ServerFailureDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fa="http://schemas.fontawesome.io/icons/"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile.Windows"
mc:Ignorable="d" Background="Transparent" Height="750" Width="1000" FontSize="16" ResizeMode="NoResize" Topmost="True" AllowsTransparency="True" WindowStyle="None">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Templates/Colors.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid Background="#66000000">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" />
<Grid Grid.Row="1" Background="White">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<fa:ImageAwesome Grid.Column="0" Foreground="LightGray" Icon="Warning" Margin="50" Width="75" />
<WrapPanel Grid.Column="1" Margin="0,0,50,0" Orientation="Vertical" VerticalAlignment="Center">
<TextBlock x:Name="Message" TextWrapping="WrapWithOverflow" />
<TextBlock x:Name="Info" FontFamily="Courier New" Margin="0,10,0,0" TextWrapping="WrapWithOverflow" />
</WrapPanel>
</Grid>
</Grid>
<Grid Grid.Row="2" Background="{StaticResource BackgroundBrush}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<WrapPanel Orientation="Horizontal" Margin="50,25" HorizontalAlignment="Right" VerticalAlignment="Center">
<Button x:Name="RetryButton" Cursor="Hand" Margin="10,0" Padding="10,5" MinWidth="75" />
<Button x:Name="FallbackButton" Cursor="Hand" Margin="0,0,10,0" Padding="10,5" MinWidth="75" />
<Button x:Name="AbortButton" Cursor="Hand" Padding="10,5" MinWidth="75" />
</WrapPanel>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,119 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* 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.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Windows;
using SafeExamBrowser.UserInterface.Contracts.Windows.Data;
namespace SafeExamBrowser.UserInterface.Mobile.Windows
{
public partial class ServerFailureDialog : Window, IServerFailureDialog
{
private readonly IText text;
public ServerFailureDialog(string info, bool showFallback, IText text)
{
this.text = text;
InitializeComponent();
InitializeDialog(info, showFallback);
}
public ServerFailureDialogResult Show(IWindow parent = null)
{
return Dispatcher.Invoke(() =>
{
var result = new ServerFailureDialogResult { Success = false };
if (parent is Window)
{
Owner = parent as Window;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
}
InitializeBounds();
if (ShowDialog() is true)
{
result.Abort = Tag as string == nameof(AbortButton);
result.Fallback = Tag as string == nameof(FallbackButton);
result.Retry = Tag as string == nameof(RetryButton);
result.Success = true;
}
else
{
result.Abort = true;
}
return result;
});
}
private void InitializeBounds()
{
Left = 0;
Top = 0;
Height = SystemParameters.PrimaryScreenHeight;
Width = SystemParameters.PrimaryScreenWidth;
}
private void InitializeDialog(string info, bool showFallback)
{
InitializeBounds();
Info.Text = info;
Message.Text = text.Get(TextKey.ServerFailureDialog_Message);
Title = text.Get(TextKey.ServerFailureDialog_Title);
AbortButton.Click += AbortButton_Click;
AbortButton.Content = text.Get(TextKey.ServerFailureDialog_Abort);
FallbackButton.Click += FallbackButton_Click;
FallbackButton.Content = text.Get(TextKey.ServerFailureDialog_Fallback);
FallbackButton.Visibility = showFallback ? Visibility.Visible : Visibility.Collapsed;
Loaded += (o, args) => Activate();
RetryButton.Click += RetryButton_Click;
RetryButton.Content = text.Get(TextKey.ServerFailureDialog_Retry);
SystemParameters.StaticPropertyChanged += SystemParameters_StaticPropertyChanged;
}
private void AbortButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = true;
Tag = nameof(AbortButton);
Close();
}
private void FallbackButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = true;
Tag = nameof(FallbackButton);
Close();
}
private void RetryButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = true;
Tag = nameof(RetryButton);
Close();
}
private void SystemParameters_StaticPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(SystemParameters.WorkArea))
{
Dispatcher.InvokeAsync(InitializeBounds);
}
}
}
}

View File

@@ -0,0 +1,28 @@
<Window x:Class="SafeExamBrowser.UserInterface.Mobile.Windows.SplashScreen" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile"
mc:Ignorable="d"
Title="SplashScreen" Background="White" FontSize="16" Height="300" Width="500" WindowStyle="None" WindowStartupLocation="CenterScreen"
Cursor="Wait" Icon="../Images/SafeExamBrowser.ico" ResizeMode="NoResize" Topmost="True">
<Border BorderBrush="DodgerBlue" BorderThickness="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="50" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="0,25">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="225" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Grid.ColumnSpan="2" Source="pack://application:,,,/SafeExamBrowser.UserInterface.Mobile;component/Images/SplashScreen.png" />
<TextBlock x:Name="InfoTextBlock" Grid.Column="1" Foreground="Gray" Margin="10,85,10,10" TextWrapping="Wrap" />
</Grid>
<ProgressBar x:Name="ProgressBar" Grid.Row="1" Minimum="0" Maximum="{Binding Path=MaxProgress}" Value="{Binding Path=CurrentProgress}" IsIndeterminate="{Binding Path=IsIndeterminate}" BorderThickness="0" />
<TextBlock x:Name="StatusTextBlock" Grid.Row="1" Text="{Binding Path=Status}" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</Border>
</Window>

View File

@@ -0,0 +1,146 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* 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 System.Windows.Media;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Windows;
using SafeExamBrowser.UserInterface.Contracts.Windows.Events;
using SafeExamBrowser.UserInterface.Mobile.ViewModels;
namespace SafeExamBrowser.UserInterface.Mobile.Windows
{
internal partial class SplashScreen : Window, ISplashScreen
{
private readonly ProgressIndicatorViewModel model;
private readonly IText text;
private bool allowClose;
private AppConfig appConfig;
private WindowClosedEventHandler closed;
private WindowClosingEventHandler closing;
public AppConfig AppConfig
{
set
{
Dispatcher.Invoke(() =>
{
appConfig = value;
UpdateAppInfo();
});
}
}
event WindowClosedEventHandler IWindow.Closed
{
add { closed += value; }
remove { closed -= value; }
}
event WindowClosingEventHandler IWindow.Closing
{
add { closing += value; }
remove { closing -= value; }
}
internal SplashScreen(IText text, AppConfig appConfig = null)
{
this.appConfig = appConfig;
this.model = new ProgressIndicatorViewModel();
this.text = text;
InitializeComponent();
InitializeSplashScreen();
}
public void BringToForeground()
{
Dispatcher.Invoke(Activate);
}
public new void Close()
{
Dispatcher.Invoke(() =>
{
allowClose = true;
model.BusyIndication = false;
closing?.Invoke();
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.IsIndeterminate = false;
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;
Closed += (o, args) => closed?.Invoke();
Closing += (o, args) => args.Cancel = !allowClose;
}
private void UpdateAppInfo()
{
if (appConfig != null)
{
InfoTextBlock.Inlines.Add(new Run($"Version {appConfig.ProgramInformationalVersion}") { Foreground = Brushes.DimGray });
InfoTextBlock.Inlines.Add(new LineBreak());
InfoTextBlock.Inlines.Add(new LineBreak());
InfoTextBlock.Inlines.Add(new Run(appConfig.ProgramCopyright) { FontSize = 12 });
}
}
}
}

View File

@@ -0,0 +1,35 @@
<Window x:Class="SafeExamBrowser.UserInterface.Mobile.Windows.Taskbar" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar"
xmlns:s="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d" Title="Taskbar" Background="{DynamicResource BackgroundBrush}" Height="60" FontSize="16" Width="750" WindowStyle="None"
KeyDown="Window_KeyDown" KeyUp="Window_KeyUp"
Topmost="True" ResizeMode="NoResize" Icon="../Images/SafeExamBrowser.ico">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Templates/Colors.xaml" />
<ResourceDictionary Source="../Templates/ScrollViewers.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ScrollViewer Grid.Column="0" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled" Template="{StaticResource SmallBarScrollViewer}">
<StackPanel x:Name="ApplicationStackPanel" Orientation="Horizontal" />
</ScrollViewer>
<StackPanel Grid.Column="1" x:Name="NotificationStackPanel" Orientation="Horizontal" VerticalAlignment="Stretch" />
<StackPanel Grid.Column="2" x:Name="SystemControlStackPanel" Orientation="Horizontal" VerticalAlignment="Stretch" />
<local:Clock Grid.Column="3" x:Name="Clock" Foreground="{StaticResource SecondaryTextBrush}" Padding="10,0,10,0" />
<local:QuitButton Grid.Column="4" x:Name="QuitButton" />
</Grid>
</Window>

View File

@@ -0,0 +1,262 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* 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 SafeExamBrowser.Browser.Contracts.Events;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Shell;
using SafeExamBrowser.UserInterface.Contracts.Shell.Events;
using SafeExamBrowser.UserInterface.Shared.Utilities;
namespace SafeExamBrowser.UserInterface.Mobile.Windows
{
internal partial class Taskbar : Window, ITaskbar
{
private bool allowClose;
private readonly ILogger logger;
private bool isQuitButtonFocusedAtKeyDown;
private bool isFirstChildFocusedAtKeyDown;
public bool ShowClock
{
set { Dispatcher.Invoke(() => Clock.Visibility = value ? Visibility.Visible : Visibility.Collapsed); }
}
public bool ShowQuitButton
{
set { Dispatcher.Invoke(() => QuitButton.Visibility = value ? Visibility.Visible : Visibility.Collapsed); }
}
public event LoseFocusRequestedEventHandler LoseFocusRequested;
public event QuitButtonClickedEventHandler QuitButtonClicked;
internal Taskbar(ILogger logger)
{
this.logger = logger;
InitializeComponent();
InitializeTaskbar();
}
public void AddApplicationControl(IApplicationControl control, bool atFirstPosition = false)
{
if (control is UIElement uiElement)
{
if (atFirstPosition)
{
ApplicationStackPanel.Children.Insert(0, uiElement);
}
else
{
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 void Focus(bool fromTop)
{
Dispatcher.BeginInvoke((Action) (() =>
{
Activate();
if (fromTop && ApplicationStackPanel.Children.Count > 0)
{
ApplicationStackPanel.Children[0].Focus();
}
else
{
QuitButton.Focus();
}
}));
}
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}).");
});
}
private void InitializeTaskbar()
{
Closing += Taskbar_Closing;
Loaded += (o, args) => InitializeBounds();
QuitButton.Clicked += QuitButton_Clicked;
}
public void InitializeText(IText text)
{
Dispatcher.Invoke(() =>
{
var txt = text.Get(TextKey.Shell_QuitButton);
QuitButton.ToolTip = txt;
QuitButton.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, txt);
});
}
public void Register(ITaskbarActivator activator)
{
activator.Activated += Activator_Activated;
}
public new void Show()
{
Dispatcher.Invoke(base.Show);
}
private void Activator_Activated()
{
(this as ITaskbar).Focus(true);
}
private void QuitButton_Clicked(CancelEventArgs args)
{
QuitButtonClicked?.Invoke(args);
allowClose = !args.Cancel;
}
private void Taskbar_Closing(object sender, CancelEventArgs e)
{
if (allowClose)
{
foreach (var child in SystemControlStackPanel.Children)
{
if (child is ISystemControl systemControl)
{
systemControl.Close();
}
}
}
else
{
e.Cancel = true;
}
}
private void Window_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
isQuitButtonFocusedAtKeyDown = QuitButton.IsKeyboardFocusWithin;
isFirstChildFocusedAtKeyDown = ApplicationStackPanel.Children.Count > 0 && ApplicationStackPanel.Children[0].IsKeyboardFocusWithin;
}
private void Window_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key == System.Windows.Input.Key.Tab)
{
var shift = System.Windows.Input.Keyboard.IsKeyDown(System.Windows.Input.Key.LeftShift);
var hasFocus = ApplicationStackPanel.Children.Count > 0 && ApplicationStackPanel.Children[0].IsKeyboardFocusWithin;
if (!shift && hasFocus && isQuitButtonFocusedAtKeyDown)
{
LoseFocusRequested?.Invoke(true);
e.Handled = true;
}
else if (shift && QuitButton.IsKeyboardFocusWithin && isFirstChildFocusedAtKeyDown)
{
LoseFocusRequested?.Invoke(false);
e.Handled = true;
}
}
isQuitButtonFocusedAtKeyDown = false;
isFirstChildFocusedAtKeyDown = false;
}
private bool SetFocusWithin(UIElement uIElement)
{
if (uIElement.Focusable)
{
uIElement.Focus();
return true;
}
if (uIElement is System.Windows.Controls.Panel)
{
var panel = uIElement as System.Windows.Controls.Panel;
for (var i = 0; i < panel.Children.Count; i++)
{
if (SetFocusWithin(panel.Children[i]))
{
return true;
}
}
return false;
}
else if (uIElement is System.Windows.Controls.ContentControl)
{
var control = uIElement as System.Windows.Controls.ContentControl;
var content = control.Content as UIElement;
if (content != null)
{
return SetFocusWithin(content);
}
}
return false;
}
}
}

View File

@@ -0,0 +1,19 @@
<Window x:Class="SafeExamBrowser.UserInterface.Mobile.Windows.Taskview" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile"
mc:Ignorable="d" AllowsTransparency="True" Background="{DynamicResource BackgroundTransparentBrush}" BorderBrush="DodgerBlue" BorderThickness="1"
Title="Taskview" Topmost="True" Height="450" SizeToContent="WidthAndHeight" Width="800" WindowStartupLocation="CenterScreen" WindowStyle="None">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Templates/Colors.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid>
<StackPanel Name="Rows" Margin="10" Orientation="Vertical" />
</Grid>
</Window>

View File

@@ -0,0 +1,197 @@
/*
* Copyright (c) 2024 ETH Zürich, IT Services
*
* 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.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using SafeExamBrowser.Applications.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Shell;
using SafeExamBrowser.UserInterface.Mobile.Controls.Taskview;
namespace SafeExamBrowser.UserInterface.Mobile.Windows
{
internal partial class Taskview : Window, ITaskview
{
private readonly IList<IApplication<IApplicationWindow>> applications;
private readonly LinkedList<WindowControl> controls;
private LinkedListNode<WindowControl> current;
internal IntPtr Handle { get; private set; }
internal Taskview()
{
applications = new List<IApplication<IApplicationWindow>>();
controls = new LinkedList<WindowControl>();
InitializeComponent();
InitializeTaskview();
}
public void Add(IApplication<IApplicationWindow> application)
{
application.WindowsChanged += Application_WindowsChanged;
applications.Add(application);
}
public void Register(ITaskviewActivator activator)
{
activator.Deactivated += Activator_Deactivated;
activator.NextActivated += Activator_Next;
activator.PreviousActivated += Activator_Previous;
}
private void Application_WindowsChanged()
{
Dispatcher.InvokeAsync(Update);
}
private void Activator_Deactivated()
{
Dispatcher.InvokeAsync(ActivateAndHide);
}
private void Activator_Next()
{
Dispatcher.InvokeAsync(SelectNext);
}
private void Activator_Previous()
{
Dispatcher.InvokeAsync(SelectPrevious);
}
private void ActivateAndHide()
{
if (IsVisible)
{
Activate();
current?.Value.Activate();
Hide();
}
}
private void InitializeTaskview()
{
Loaded += (o, args) =>
{
Handle = new WindowInteropHelper(this).Handle;
Update();
};
}
private void SelectNext()
{
ShowConditional();
if (current != null)
{
current.Value.Deselect();
current = current.Next ?? controls.First;
current.Value.Select();
}
}
private void SelectPrevious()
{
ShowConditional();
if (current != null)
{
current.Value.Deselect();
current = current.Previous ?? controls.Last;
current.Value.Select();
}
}
private void ShowConditional()
{
if (controls.Any() && Visibility != Visibility.Visible)
{
Show();
Activate();
}
}
private void Update()
{
ClearTaskview();
LoadControls();
UpdateLocation();
}
private void ClearTaskview()
{
foreach (var control in controls)
{
control.Destroy();
}
controls.Clear();
Rows.Children.Clear();
}
private void LoadControls()
{
var windows = GetAllWindows();
var maxColumns = Math.Ceiling(Math.Sqrt(windows.Count));
while (windows.Any())
{
var row = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Center };
Rows.Children.Add(row);
for (var column = 0; column < maxColumns && windows.Any(); column++)
{
var window = windows.Pop();
var control = new WindowControl(window, this);
controls.AddLast(control);
row.Children.Add(control);
}
}
current = controls.First;
current?.Value.Select();
}
private void UpdateLocation()
{
if (controls.Any())
{
UpdateLayout();
Left = (SystemParameters.WorkArea.Width - Width) / 2 + SystemParameters.WorkArea.Left;
Top = (SystemParameters.WorkArea.Height - Height) / 2 + SystemParameters.WorkArea.Top;
}
else
{
Hide();
}
}
private Stack<IApplicationWindow> GetAllWindows()
{
var stack = new Stack<IApplicationWindow>();
foreach (var application in applications)
{
foreach (var window in application.GetWindows())
{
stack.Push(window);
}
}
return stack;
}
}
}