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,31 @@
<UserControl x:Class="SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar.ApplicationControl" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile.Controls"
xmlns:s="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d" d:DesignHeight="40" d:DesignWidth="50">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Templates/Buttons.xaml" />
<ResourceDictionary Source="../../Templates/Colors.xaml" />
<ResourceDictionary Source="../../Templates/ScrollViewers.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Popup x:Name="WindowPopup" IsOpen="False" Placement="Custom" PlacementTarget="{Binding ElementName=Button}">
<Border Background="LightGray" BorderBrush="Gray" BorderThickness="1,1,1,0">
<ScrollViewer MaxHeight="400" VerticalScrollBarVisibility="Auto" Template="{StaticResource SmallBarScrollViewer}">
<StackPanel x:Name="WindowStackPanel" />
</ScrollViewer>
</Border>
</Popup>
<Button x:Name="Button" Background="{StaticResource BackgroundBrush}" Padding="4" Template="{StaticResource TaskbarButton}" Width="60" />
<Grid>
<Rectangle x:Name="ActiveBar" Cursor="Hand" Height="2.5" Width="40" VerticalAlignment="Bottom" Fill="DodgerBlue" Visibility="Collapsed" />
</Grid>
</Grid>
</UserControl>

View File

@@ -0,0 +1,118 @@
/*
* 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.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Automation;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Threading;
using SafeExamBrowser.Applications.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Shell;
using SafeExamBrowser.UserInterface.Shared.Utilities;
namespace SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar
{
internal partial class ApplicationControl : UserControl, IApplicationControl
{
private readonly IApplication<IApplicationWindow> application;
private IApplicationWindow single;
internal ApplicationControl(IApplication<IApplicationWindow> application)
{
this.application = application;
InitializeComponent();
InitializeApplicationControl();
}
private void InitializeApplicationControl()
{
var originalBrush = Button.Background;
application.WindowsChanged += Application_WindowsChanged;
ActiveBar.MouseLeave += (o, args) => WindowPopup.IsOpen &= WindowPopup.IsMouseOver || Button.IsMouseOver;
Button.Click += Button_Click;
Button.Content = IconResourceLoader.Load(application.Icon);
Button.MouseEnter += (o, args) => WindowPopup.IsOpen = WindowStackPanel.Children.Count > 0;
Button.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => WindowPopup.IsOpen = WindowPopup.IsMouseOver || ActiveBar.IsMouseOver));
Button.ToolTip = application.Tooltip;
WindowPopup.CustomPopupPlacementCallback = new CustomPopupPlacementCallback(WindowPopup_PlacementCallback);
WindowPopup.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() => WindowPopup.IsOpen = IsMouseOver));
if (application.Tooltip != default)
{
AutomationProperties.SetName(Button, application.Tooltip);
}
WindowPopup.Opened += (o, args) =>
{
ActiveBar.Width = double.NaN;
Background = Brushes.LightGray;
Button.Background = Brushes.LightGray;
};
WindowPopup.Closed += (o, args) =>
{
ActiveBar.Width = 40;
Background = originalBrush;
Button.Background = originalBrush;
};
}
private void Application_WindowsChanged()
{
Dispatcher.InvokeAsync(Update);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
if (WindowStackPanel.Children.Count == 0)
{
application.Start();
}
else if (WindowStackPanel.Children.Count == 1)
{
single?.Activate();
}
}
private CustomPopupPlacement[] WindowPopup_PlacementCallback(Size popupSize, Size targetSize, Point offset)
{
return new[]
{
new CustomPopupPlacement(new Point(targetSize.Width / 2 - popupSize.Width / 2, -popupSize.Height), PopupPrimaryAxis.None)
};
}
private void Update()
{
var windows = application.GetWindows();
ActiveBar.Visibility = windows.Any() ? Visibility.Visible : Visibility.Collapsed;
WindowStackPanel.Children.Clear();
foreach (var window in windows)
{
WindowStackPanel.Children.Add(new ApplicationWindowButton(window));
}
if (WindowStackPanel.Children.Count == 1)
{
single = windows.First();
}
else
{
single = default;
}
}
}
}

View File

@@ -0,0 +1,24 @@
<UserControl x:Class="SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar.ApplicationWindowButton" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile.Controls"
mc:Ignorable="d" d:DesignWidth="250">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Templates/Buttons.xaml" />
<ResourceDictionary Source="../../Templates/Colors.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Button x:Name="Button" Background="Transparent" Height="60" Padding="10" Template="{StaticResource TaskbarButton}">
<StackPanel Orientation="Horizontal">
<ContentControl x:Name="Icon" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0,0,10,0" Width="20" />
<TextBlock x:Name="Text" HorizontalAlignment="Left" VerticalAlignment="Center" Padding="5" MaxWidth="350" TextTrimming="CharacterEllipsis" />
</StackPanel>
</Button>
</Grid>
</UserControl>

View File

@@ -0,0 +1,58 @@
/*
* 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.Controls;
using SafeExamBrowser.Applications.Contracts;
using SafeExamBrowser.Core.Contracts.Resources.Icons;
using SafeExamBrowser.UserInterface.Shared.Utilities;
namespace SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar
{
internal partial class ApplicationWindowButton : UserControl
{
private IApplicationWindow window;
internal ApplicationWindowButton(IApplicationWindow window)
{
this.window = window;
InitializeComponent();
InitializeApplicationInstanceButton();
}
private void InitializeApplicationInstanceButton()
{
Button.Click += Button_Click;
Button.ToolTip = window.Title;
Icon.Content = IconResourceLoader.Load(window.Icon);
window.IconChanged += Instance_IconChanged;
window.TitleChanged += Window_TitleChanged;
Text.Text = window.Title;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
window.Activate();
}
private void Instance_IconChanged(IconResource icon)
{
Dispatcher.InvokeAsync(() => Icon.Content = IconResourceLoader.Load(icon));
}
private void Window_TitleChanged(string title)
{
Dispatcher.InvokeAsync(() =>
{
Text.Text = title;
Button.ToolTip = title;
});
}
}
}

View File

@@ -0,0 +1,43 @@
<UserControl x:Class="SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar.AudioControl" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fa="http://schemas.fontawesome.io/icons/"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile.Controls"
mc:Ignorable="d" d:DesignHeight="40" d:DesignWidth="40">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Templates/Buttons.xaml" />
<ResourceDictionary Source="../../Templates/Colors.xaml" />
<ResourceDictionary Source="../../Templates/ScrollViewers.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Popup x:Name="Popup" IsOpen="False" Placement="Custom" PlacementTarget="{Binding ElementName=Button}" KeyDown="Popup_KeyDown">
<Border Background="LightGray" BorderBrush="Gray" BorderThickness="1,1,1,0">
<StackPanel Orientation="Vertical">
<TextBlock x:Name="AudioDeviceName" Margin="5" TextAlignment="Center" />
<StackPanel Orientation="Horizontal" Height="60" Margin="5,0">
<Button x:Name="MuteButton" Background="Transparent" Padding="5" Template="{StaticResource TaskbarButton}" Width="60">
<ContentControl x:Name="PopupIcon" Focusable="False" />
</Button>
<Slider x:Name="Volume" Grid.Column="1" Orientation="Horizontal" TickFrequency="1" Maximum="100" IsSnapToTickEnabled="True" KeyDown="Volume_KeyDown"
IsMoveToPointEnabled="True" VerticalAlignment="Center" Width="125" Thumb.DragStarted="Volume_DragStarted" Thumb.DragCompleted="Volume_DragCompleted">
<Slider.LayoutTransform>
<ScaleTransform ScaleX="2" ScaleY="2" CenterX="0" CenterY="0"/>
</Slider.LayoutTransform>
</Slider>
<TextBlock Grid.Column="2" FontWeight="DemiBold" FontSize="20" Text="{Binding ElementName=Volume, Path=Value}"
TextAlignment="Center" VerticalAlignment="Center" Width="60" />
</StackPanel>
</StackPanel>
</Border>
</Popup>
<Button x:Name="Button" Background="Transparent" Padding="5" Template="{StaticResource TaskbarButton}" ToolTipService.ShowOnDisabled="True" Width="60">
<ContentControl x:Name="ButtonIcon" Focusable="False" />
</Button>
</Grid>
</UserControl>

View File

@@ -0,0 +1,226 @@
/*
* 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.Tasks;
using System.Windows;
using System.Windows.Automation;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using SafeExamBrowser.Core.Contracts.Resources.Icons;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.SystemComponents.Contracts.Audio;
using SafeExamBrowser.UserInterface.Contracts.Shell;
using SafeExamBrowser.UserInterface.Shared.Utilities;
namespace SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar
{
internal partial class AudioControl : UserControl, ISystemControl
{
private readonly IAudio audio;
private readonly IText text;
private bool muted;
private IconResource MutedIcon;
private IconResource NoDeviceIcon;
internal AudioControl(IAudio audio, IText text)
{
this.audio = audio;
this.text = text;
InitializeComponent();
InitializeAudioControl();
}
public void Close()
{
Popup.IsOpen = false;
}
private void InitializeAudioControl()
{
var originalBrush = Button.Background;
audio.VolumeChanged += Audio_VolumeChanged;
Button.Click += (o, args) => Popup.IsOpen = !Popup.IsOpen;
var lastOpenedBySpacePress = false;
Button.PreviewKeyDown += (o, args) =>
{
// For some reason, the popup immediately closes again if opened by a Space Bar key event - as a mitigation,
// we record the space bar event and leave the popup open for at least 3 seconds.
if (args.Key == System.Windows.Input.Key.Space)
{
lastOpenedBySpacePress = true;
}
};
Button.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() =>
{
if (Popup.IsOpen && lastOpenedBySpacePress)
{
return;
}
Popup.IsOpen = Popup.IsMouseOver;
}));
MuteButton.Click += MuteButton_Click;
MutedIcon = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Mobile;component/Images/Audio_Muted.xaml") };
NoDeviceIcon = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Mobile;component/Images/Audio_NoDevice.xaml") };
Popup.CustomPopupPlacementCallback = new CustomPopupPlacementCallback(Popup_PlacementCallback);
Popup.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() =>
{
if (Popup.IsOpen && lastOpenedBySpacePress)
{
return;
}
Popup.IsOpen = IsMouseOver;
}));
Volume.ValueChanged += Volume_ValueChanged;
Popup.Opened += (o, args) =>
{
Background = Brushes.LightGray;
Button.Background = Brushes.LightGray;
Volume.Focus();
};
Popup.Closed += (o, args) =>
{
Background = originalBrush;
Button.Background = originalBrush;
lastOpenedBySpacePress = false;
};
if (audio.HasOutputDevice)
{
AudioDeviceName.Text = audio.DeviceFullName;
Button.IsEnabled = true;
UpdateVolume(audio.OutputVolume, audio.OutputMuted);
}
else
{
AudioDeviceName.Text = text.Get(TextKey.SystemControl_AudioDeviceNotFound);
Button.IsEnabled = false;
Button.ToolTip = text.Get(TextKey.SystemControl_AudioDeviceNotFound);
ButtonIcon.Content = IconResourceLoader.Load(NoDeviceIcon);
}
}
private void Audio_VolumeChanged(double volume, bool muted)
{
Dispatcher.InvokeAsync(() => UpdateVolume(volume, muted));
}
private void MuteButton_Click(object sender, RoutedEventArgs e)
{
if (muted)
{
audio.Unmute();
}
else
{
audio.Mute();
}
}
private CustomPopupPlacement[] Popup_PlacementCallback(Size popupSize, Size targetSize, Point offset)
{
return new[]
{
new CustomPopupPlacement(new Point(targetSize.Width / 2 - popupSize.Width / 2, -popupSize.Height), PopupPrimaryAxis.None)
};
}
private void Volume_DragStarted(object sender, DragStartedEventArgs e)
{
Volume.ValueChanged -= Volume_ValueChanged;
}
private void Volume_DragCompleted(object sender, DragCompletedEventArgs e)
{
audio.SetVolume(Volume.Value / 100);
Volume.ValueChanged += Volume_ValueChanged;
}
private void Volume_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
audio.SetVolume(Volume.Value / 100);
}
private void UpdateVolume(double volume, bool muted)
{
var info = BuildInfoText(volume, muted);
this.muted = muted;
Button.ToolTip = info;
Volume.ValueChanged -= Volume_ValueChanged;
Volume.Value = Math.Round(volume * 100);
Volume.ValueChanged += Volume_ValueChanged;
AutomationProperties.SetName(Button, info);
if (muted)
{
var tooltip = text.Get(TextKey.SystemControl_AudioDeviceUnmuteTooltip);
MuteButton.ToolTip = tooltip;
PopupIcon.Content = IconResourceLoader.Load(MutedIcon);
ButtonIcon.Content = IconResourceLoader.Load(MutedIcon);
AutomationProperties.SetName(MuteButton, tooltip);
}
else
{
var tooltip = text.Get(TextKey.SystemControl_AudioDeviceMuteTooltip);
MuteButton.ToolTip = tooltip;
PopupIcon.Content = LoadIcon(volume);
ButtonIcon.Content = LoadIcon(volume);
AutomationProperties.SetName(MuteButton, tooltip);
}
}
private string BuildInfoText(double volume, bool muted)
{
var info = text.Get(muted ? TextKey.SystemControl_AudioDeviceInfoMuted : TextKey.SystemControl_AudioDeviceInfo);
info = info.Replace("%%NAME%%", audio.DeviceShortName);
info = info.Replace("%%VOLUME%%", Convert.ToString(Math.Round(volume * 100)));
return info;
}
private UIElement LoadIcon(double volume)
{
var icon = volume > 0.66 ? "100" : (volume > 0.33 ? "66" : "33");
var uri = new Uri($"pack://application:,,,/SafeExamBrowser.UserInterface.Mobile;component/Images/Audio_{icon}.xaml");
var resource = new XamlIconResource { Uri = uri };
return IconResourceLoader.Load(resource);
}
private void Popup_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key == System.Windows.Input.Key.Escape)
{
Popup.IsOpen = false;
Button.Focus();
}
}
private void Volume_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key == System.Windows.Input.Key.Enter)
{
Popup.IsOpen = false;
Button.Focus();
}
}
}
}

View File

@@ -0,0 +1,17 @@
<UserControl x:Class="SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar.Clock" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile.Controls"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid Background="Transparent" Margin="5,0" ToolTip="{Binding Path=ToolTip}">
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<TextBlock x:Name="TimeTextBlock" Grid.Row="0" Text="{Binding Path=Time}" FontWeight="Bold" HorizontalAlignment="Center" VerticalAlignment="Bottom" Focusable="True" AutomationProperties.Name="{Binding Path=Time}" />
<TextBlock x:Name="DateTextBlock" Grid.Row="1" Text="{Binding Path=Date}" HorizontalAlignment="Center" VerticalAlignment="Top" Focusable="True" AutomationProperties.Name="{Binding Path=Date}" />
</Grid>
</UserControl>

View File

@@ -0,0 +1,32 @@
/*
* 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.Controls;
using SafeExamBrowser.UserInterface.Mobile.ViewModels;
namespace SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar
{
internal partial class Clock : UserControl
{
private DateTimeViewModel model;
public Clock()
{
InitializeComponent();
InitializeControl();
}
private void InitializeControl()
{
model = new DateTimeViewModel(false);
DataContext = model;
TimeTextBlock.DataContext = model;
DateTextBlock.DataContext = model;
}
}
}

View File

@@ -0,0 +1,35 @@
<UserControl x:Class="SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar.KeyboardLayoutButton" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fa="http://schemas.fontawesome.io/icons/"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile.Controls"
mc:Ignorable="d" d:DesignHeight="40" d:DesignWidth="250">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Templates/Buttons.xaml" />
<ResourceDictionary Source="../../Templates/Colors.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Button x:Name="Button" Background="Transparent" Height="60" Padding="10,0" Template="{StaticResource TaskbarButton}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock x:Name="IsCurrentTextBlock" Grid.Column="0" FontSize="20" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Hidden">•</TextBlock>
<TextBlock x:Name="CultureCodeTextBlock" Grid.Column="1" FontWeight="Bold" HorizontalAlignment="Left" Margin="10,0,5,0" VerticalAlignment="Center" />
<StackPanel Grid.Column="2" VerticalAlignment="Center">
<TextBlock x:Name="CultureNameTextBlock" HorizontalAlignment="Left" Margin="5,0,10,0" TextDecorations="Underline" VerticalAlignment="Center" />
<StackPanel Orientation="Horizontal">
<fa:ImageAwesome Foreground="Gray" Height="10" Icon="KeyboardOutline" Margin="5,0" />
<TextBlock x:Name="LayoutNameTextBlock" Foreground="Gray" HorizontalAlignment="Left" Margin="0,0,10,0" VerticalAlignment="Center" />
</StackPanel>
</StackPanel>
</Grid>
</Button>
</UserControl>

View File

@@ -0,0 +1,51 @@
/*
* 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.Windows;
using System.Windows.Automation;
using System.Windows.Controls;
using SafeExamBrowser.SystemComponents.Contracts.Keyboard;
namespace SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar
{
internal partial class KeyboardLayoutButton : UserControl
{
private readonly IKeyboardLayout layout;
internal bool IsCurrent
{
set { IsCurrentTextBlock.Visibility = value ? Visibility.Visible : Visibility.Hidden; }
}
internal Guid LayoutId
{
get { return layout.Id; }
}
internal event EventHandler LayoutSelected;
internal KeyboardLayoutButton(IKeyboardLayout layout)
{
this.layout = layout;
InitializeComponent();
InitializeLayoutButton();
}
private void InitializeLayoutButton()
{
Button.Click += (o, args) => LayoutSelected?.Invoke(this, EventArgs.Empty);
CultureCodeTextBlock.Text = layout.CultureCode;
CultureNameTextBlock.Text = layout.CultureName;
LayoutNameTextBlock.Text = layout.LayoutName;
AutomationProperties.SetName(Button, layout.CultureName);
}
}
}

View File

@@ -0,0 +1,32 @@
<UserControl x:Class="SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar.KeyboardLayoutControl" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile.Controls"
mc:Ignorable="d" d:DesignHeight="40" d:DesignWidth="40">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Templates/Buttons.xaml" />
<ResourceDictionary Source="../../Templates/Colors.xaml" />
<ResourceDictionary Source="../../Templates/ScrollViewers.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Popup x:Name="Popup" IsOpen="False" Placement="Custom" PlacementTarget="{Binding ElementName=Button}" KeyUp="Popup_KeyUp">
<Border Background="LightGray" BorderBrush="Gray" BorderThickness="1,1,1,0">
<ScrollViewer x:Name="LayoutsScrollViewer" MaxHeight="250" VerticalScrollBarVisibility="Auto" Template="{StaticResource SmallBarScrollViewer}">
<StackPanel x:Name="LayoutsStackPanel" />
</ScrollViewer>
</Border>
</Popup>
<Button x:Name="Button" Background="Transparent" Template="{StaticResource TaskbarButton}" Padding="5" Width="60">
<Viewbox Stretch="Uniform">
<TextBlock x:Name="LayoutCultureCode" FontWeight="Bold" Margin="2" TextAlignment="Center" VerticalAlignment="Center" />
</Viewbox>
</Button>
</Grid>
</UserControl>

View File

@@ -0,0 +1,162 @@
/*
* 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.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Automation;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.SystemComponents.Contracts.Keyboard;
using SafeExamBrowser.UserInterface.Contracts.Shell;
namespace SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar
{
internal partial class KeyboardLayoutControl : UserControl, ISystemControl
{
private readonly IKeyboard keyboard;
private readonly IText text;
internal KeyboardLayoutControl(IKeyboard keyboard, IText text)
{
this.keyboard = keyboard;
this.text = text;
InitializeComponent();
InitializeKeyboardLayoutControl();
}
public void Close()
{
Dispatcher.Invoke(() => Popup.IsOpen = false);
}
private void InitializeKeyboardLayoutControl()
{
var originalBrush = Button.Background;
InitializeLayouts();
keyboard.LayoutChanged += Keyboard_LayoutChanged;
Button.Click += (o, args) =>
{
Popup.IsOpen = !Popup.IsOpen;
Task.Delay(200).ContinueWith(_ => this.Dispatcher.BeginInvoke((System.Action) (() =>
{
((LayoutsStackPanel.Children[0] as ContentControl).Content as UIElement).Focus();
})));
};
var lastOpenedBySpacePress = false;
Button.PreviewKeyDown += (o, args) =>
{
// For some reason, the popup immediately closes again if opened by a Space Bar key event - as a mitigation,
// we record the space bar event and leave the popup open for at least 3 seconds.
if (args.Key == System.Windows.Input.Key.Space)
{
lastOpenedBySpacePress = true;
}
};
Button.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() =>
{
if (Popup.IsOpen && lastOpenedBySpacePress)
{
return;
}
Popup.IsOpen = Popup.IsMouseOver;
}));
Popup.CustomPopupPlacementCallback = new CustomPopupPlacementCallback(Popup_PlacementCallback);
Popup.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() =>
{
if (Popup.IsOpen && lastOpenedBySpacePress)
{
return;
}
Popup.IsOpen = IsMouseOver;
}));
Popup.Opened += (o, args) =>
{
Background = Brushes.LightGray;
Button.Background = Brushes.LightGray;
};
Popup.Closed += (o, args) =>
{
Background = originalBrush;
Button.Background = originalBrush;
lastOpenedBySpacePress = false;
};
}
private void Keyboard_LayoutChanged(IKeyboardLayout layout)
{
Dispatcher.InvokeAsync(() => SetCurrent(layout));
}
private CustomPopupPlacement[] Popup_PlacementCallback(Size popupSize, Size targetSize, Point offset)
{
return new[]
{
new CustomPopupPlacement(new Point(targetSize.Width / 2 - popupSize.Width / 2, -popupSize.Height), PopupPrimaryAxis.None)
};
}
private void InitializeLayouts()
{
foreach (var layout in keyboard.GetLayouts())
{
var button = new KeyboardLayoutButton(layout);
button.LayoutSelected += (o, args) => ActivateLayout(layout);
LayoutsStackPanel.Children.Add(button);
if (layout.IsCurrent)
{
SetCurrent(layout);
}
}
}
private void ActivateLayout(IKeyboardLayout layout)
{
Popup.IsOpen = false;
keyboard.ActivateLayout(layout.Id);
}
private void SetCurrent(IKeyboardLayout layout)
{
var name = layout.CultureName?.Length > 3 ? String.Join(string.Empty, layout.CultureName.Split(' ').Where(s => Char.IsLetter(s.First())).Select(s => s.First())) : layout.CultureName;
var tooltip = text.Get(TextKey.SystemControl_KeyboardLayoutTooltip).Replace("%%LAYOUT%%", layout.CultureName);
foreach (var child in LayoutsStackPanel.Children)
{
if (child is KeyboardLayoutButton layoutButton)
{
layoutButton.IsCurrent = layout.Id == layoutButton.LayoutId;
}
}
LayoutCultureCode.Text = layout.CultureCode;
Button.ToolTip = tooltip;
AutomationProperties.SetName(Button, tooltip);
}
private void Popup_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key == System.Windows.Input.Key.Enter || e.Key == System.Windows.Input.Key.Escape)
{
Popup.IsOpen = false;
Button.Focus();
}
}
}
}

View File

@@ -0,0 +1,30 @@
<UserControl x:Class="SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar.NetworkButton" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile.Controls"
mc:Ignorable="d" d:DesignHeight="40" d:DesignWidth="250">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Templates/Buttons.xaml" />
<ResourceDictionary Source="../../Templates/Colors.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Button x:Name="Button" Background="Transparent" Height="60" Padding="10,0" Template="{StaticResource TaskbarButton}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock x:Name="IsCurrentTextBlock" Grid.Column="0" FontSize="20" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Hidden">•</TextBlock>
<TextBlock x:Name="SignalStrengthTextBlock" Grid.Column="1" Foreground="Gray" HorizontalAlignment="Left" Margin="10,0,5,0" VerticalAlignment="Center" />
<TextBlock x:Name="NetworkNameTextBlock" Grid.Column="2" FontWeight="Bold" HorizontalAlignment="Left" Margin="5,0,10,0" VerticalAlignment="Center" />
</Grid>
</Button>
</Grid>
</UserControl>

View File

@@ -0,0 +1,43 @@
/*
* 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.Windows;
using System.Windows.Controls;
using SafeExamBrowser.SystemComponents.Contracts.Network;
namespace SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar
{
internal partial class NetworkButton : UserControl
{
private readonly IWirelessNetwork network;
internal event EventHandler NetworkSelected;
internal NetworkButton(IWirelessNetwork network)
{
this.network = network;
InitializeComponent();
InitializeNetworkButton();
}
private void InitializeNetworkButton()
{
Button.Click += (o, args) => NetworkSelected?.Invoke(this, EventArgs.Empty);
IsCurrentTextBlock.Visibility = network.Status == ConnectionStatus.Connected ? Visibility.Visible : Visibility.Hidden;
NetworkNameTextBlock.Text = network.Name;
SignalStrengthTextBlock.Text = $"{network.SignalStrength}%";
}
public void SetFocus()
{
Button.Focus();
}
}
}

View File

@@ -0,0 +1,37 @@
<UserControl x:Class="SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar.NetworkControl" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:fa="http://schemas.fontawesome.io/icons/"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile.Controls"
mc:Ignorable="d" d:DesignHeight="40" d:DesignWidth="40">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Templates/Buttons.xaml" />
<ResourceDictionary Source="../../Templates/Colors.xaml" />
<ResourceDictionary Source="../../Templates/ScrollViewers.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Popup x:Name="Popup" IsOpen="False" Placement="Custom" PlacementTarget="{Binding ElementName=Button}">
<Border Background="LightGray" BorderBrush="Gray" BorderThickness="1,1,1,0">
<ScrollViewer MaxHeight="250" VerticalScrollBarVisibility="Auto" Template="{StaticResource SmallBarScrollViewer}">
<StackPanel x:Name="WirelessNetworksStackPanel" />
</ScrollViewer>
</Border>
</Popup>
<Button x:Name="Button" Background="Transparent" Padding="5" Template="{StaticResource TaskbarButton}" ToolTipService.ShowOnDisabled="True" Width="60">
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<Viewbox x:Name="WirelessIcon" Stretch="Uniform" Width="Auto" Visibility="Collapsed" />
<fa:ImageAwesome x:Name="WiredIcon" Icon="Tv" Margin="0,2,4,4" Visibility="Collapsed" />
<Border Background="{StaticResource BackgroundBrush}" CornerRadius="6" Height="18" HorizontalAlignment="Right" Margin="0,0,-1,1" Panel.ZIndex="10" VerticalAlignment="Bottom">
<fa:ImageAwesome x:Name="NetworkStatusIcon" />
</Border>
</Grid>
</Button>
</Grid>
</UserControl>

View File

@@ -0,0 +1,188 @@
/*
* 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.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using FontAwesome.WPF;
using SafeExamBrowser.Core.Contracts.Resources.Icons;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.SystemComponents.Contracts.Network;
using SafeExamBrowser.UserInterface.Contracts.Shell;
using SafeExamBrowser.UserInterface.Shared.Utilities;
namespace SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar
{
internal partial class NetworkControl : UserControl, ISystemControl
{
private readonly INetworkAdapter adapter;
private readonly IText text;
internal NetworkControl(INetworkAdapter adapter, IText text)
{
this.adapter = adapter;
this.text = text;
InitializeComponent();
InitializeWirelessNetworkControl();
}
public void Close()
{
Dispatcher.InvokeAsync(() => Popup.IsOpen = false);
}
private void InitializeWirelessNetworkControl()
{
var lastOpenedBySpacePress = false;
var originalBrush = Button.Background;
adapter.Changed += () => Dispatcher.InvokeAsync(Update);
Button.Click += (o, args) => Popup.IsOpen = !Popup.IsOpen;
Button.PreviewKeyDown += (o, args) =>
{
// For some reason, the popup immediately closes again if opened by a Space Bar key event - as a mitigation,
// we record the space bar event and leave the popup open for at least 3 seconds.
if (args.Key == System.Windows.Input.Key.Space)
{
lastOpenedBySpacePress = true;
}
};
Button.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() =>
{
if (Popup.IsOpen && lastOpenedBySpacePress)
{
return;
}
Popup.IsOpen = Popup.IsMouseOver;
}));
Popup.Closed += (o, args) =>
{
adapter.StopWirelessNetworkScanning();
Background = originalBrush;
Button.Background = originalBrush;
lastOpenedBySpacePress = false;
};
Popup.CustomPopupPlacementCallback = new CustomPopupPlacementCallback(Popup_PlacementCallback);
Popup.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() =>
{
if (Popup.IsOpen && lastOpenedBySpacePress)
{
return;
}
Popup.IsOpen = IsMouseOver;
}));
Popup.Opened += (o, args) =>
{
adapter.StartWirelessNetworkScanning();
Background = Brushes.LightGray;
Button.Background = Brushes.LightGray;
Task.Delay(100).ContinueWith((task) => Dispatcher.Invoke(() =>
{
if (WirelessNetworksStackPanel.Children.Count > 0)
{
(WirelessNetworksStackPanel.Children[0] as NetworkButton)?.SetFocus();
}
}));
};
WirelessIcon.Child = GetWirelessIcon(0);
Update();
}
private CustomPopupPlacement[] Popup_PlacementCallback(Size popupSize, Size targetSize, Point offset)
{
return new[]
{
new CustomPopupPlacement(new Point(targetSize.Width / 2 - popupSize.Width / 2, -popupSize.Height), PopupPrimaryAxis.None)
};
}
private void Update()
{
switch (adapter.Type)
{
case ConnectionType.Wired:
Button.IsEnabled = false;
UpdateText(text.Get(TextKey.SystemControl_NetworkWiredConnected));
WiredIcon.Visibility = Visibility.Visible;
WirelessIcon.Visibility = Visibility.Collapsed;
break;
case ConnectionType.Wireless:
Button.IsEnabled = true;
WiredIcon.Visibility = Visibility.Collapsed;
WirelessIcon.Visibility = Visibility.Visible;
break;
default:
Button.IsEnabled = false;
UpdateText(text.Get(TextKey.SystemControl_NetworkNotAvailable));
WiredIcon.Visibility = Visibility.Visible;
WirelessIcon.Visibility = Visibility.Collapsed;
break;
}
switch (adapter.Status)
{
case ConnectionStatus.Connected:
UpdateText(text.Get(TextKey.SystemControl_NetworkWiredConnected));
NetworkStatusIcon.Rotation = 0;
NetworkStatusIcon.Source = ImageAwesome.CreateImageSource(FontAwesomeIcon.Globe, Brushes.Green);
NetworkStatusIcon.Spin = false;
break;
case ConnectionStatus.Connecting:
UpdateText(text.Get(TextKey.SystemControl_NetworkWirelessConnecting));
NetworkStatusIcon.Rotation = 0;
NetworkStatusIcon.Source = ImageAwesome.CreateImageSource(FontAwesomeIcon.Cog, Brushes.DimGray);
NetworkStatusIcon.Spin = true;
NetworkStatusIcon.SpinDuration = 2;
break;
default:
UpdateText(text.Get(TextKey.SystemControl_NetworkDisconnected));
NetworkStatusIcon.Source = ImageAwesome.CreateImageSource(FontAwesomeIcon.Ban, Brushes.DarkOrange);
NetworkStatusIcon.Spin = false;
WirelessIcon.Child = GetWirelessIcon(0);
break;
}
WirelessNetworksStackPanel.Children.Clear();
foreach (var network in adapter.GetWirelessNetworks())
{
var button = new NetworkButton(network);
button.NetworkSelected += (o, args) => adapter.ConnectToWirelessNetwork(network.Name);
if (network.Status == ConnectionStatus.Connected)
{
WirelessIcon.Child = GetWirelessIcon(network.SignalStrength);
UpdateText(text.Get(TextKey.SystemControl_NetworkWirelessConnected).Replace("%%NAME%%", network.Name));
}
WirelessNetworksStackPanel.Children.Add(button);
}
}
private void UpdateText(string text)
{
Button.ToolTip = text;
Button.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, text);
}
private UIElement GetWirelessIcon(int signalStrength)
{
var icon = signalStrength > 66 ? "100" : (signalStrength > 33 ? "66" : (signalStrength > 0 ? "33" : "0"));
var uri = new Uri($"pack://application:,,,/SafeExamBrowser.UserInterface.Mobile;component/Images/WiFi_{icon}.xaml");
var resource = new XamlIconResource { Uri = uri };
return IconResourceLoader.Load(resource);
}
}
}

View File

@@ -0,0 +1,20 @@
<UserControl x:Class="SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar.NotificationButton" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile.Controls"
mc:Ignorable="d" d:DesignHeight="40" d:DesignWidth="40">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Templates/Buttons.xaml" />
<ResourceDictionary Source="../../Templates/Colors.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Button x:Name="IconButton" Background="{StaticResource BackgroundBrush}" Click="IconButton_Click" Padding="7.5"
Template="{StaticResource TaskbarButton}" ToolTipService.ShowOnDisabled="True" Width="60" />
</Grid>
</UserControl>

View File

@@ -0,0 +1,53 @@
/*
* 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.Automation;
using System.Windows.Controls;
using SafeExamBrowser.Core.Contracts.Notifications;
using SafeExamBrowser.UserInterface.Contracts.Shell;
using SafeExamBrowser.UserInterface.Shared.Utilities;
namespace SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar
{
internal partial class NotificationButton : UserControl, INotificationControl
{
private readonly INotification notification;
internal NotificationButton(INotification notification)
{
this.notification = notification;
InitializeComponent();
InitializeNotification();
UpdateNotification();
}
private void IconButton_Click(object sender, RoutedEventArgs e)
{
if (notification.CanActivate)
{
notification.Activate();
}
}
private void InitializeNotification()
{
notification.NotificationChanged += () => Dispatcher.Invoke(UpdateNotification);
}
private void UpdateNotification()
{
IconButton.Content = IconResourceLoader.Load(notification.IconResource);
IconButton.IsEnabled = notification.CanActivate;
IconButton.ToolTip = notification.Tooltip;
AutomationProperties.SetName(this, notification.Tooltip);
}
}
}

View File

@@ -0,0 +1,63 @@
<UserControl x:Class="SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar.PowerSupplyControl" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile.Controls"
mc:Ignorable="d" d:DesignHeight="40" d:DesignWidth="40" Focusable="True" IsTabStop="True">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Templates/Buttons.xaml" />
<ResourceDictionary Source="../../Templates/Colors.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Popup x:Name="Popup" IsOpen="False" Placement="Custom" PlacementTarget="{Binding ElementName=Button}">
<Border Background="LightGray" BorderBrush="Gray" BorderThickness="1,1,1,0">
<Grid MaxWidth="250" Margin="20,10,20,20">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Background="Transparent" BorderBrush="LightGray" Click="Button_Click" Cursor="Hand" FontWeight="Bold"
Foreground="Gray" HorizontalAlignment="Center" Margin="0,0,0,5" Width="20">X</Button>
</Grid>
<TextBlock Grid.Row="1" x:Name="PopupText" TextWrapping="Wrap" />
</Grid>
</Border>
</Popup>
<Button x:Name="Button" Background="Transparent" IsEnabled="False" Padding="5" Template="{StaticResource TaskbarButton}" ToolTipService.ShowOnDisabled="True">
<Viewbox Stretch="Uniform" Width="Auto">
<Canvas Height="40" Width="40">
<Viewbox Stretch="Uniform" Width="40" Panel.ZIndex="2">
<Canvas Width="1024.000" Height="1024.000">
<Canvas.LayoutTransform>
<RotateTransform Angle="180" />
</Canvas.LayoutTransform>
<Canvas>
<Path Data=" M 0.000,0.000 L 1024.000,0.000 L 1024.000,1024.000 L 0.000,1024.000 L 0.000,0.000 Z"/>
<Path StrokeThickness="35.0" Stroke="#ff000000" StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeLineJoin="Round" Data=" M 112.000,300.000 L 951.000,300.000 C 970.330,300.000 986.000,315.670 986.000,335.000 L 986.000,689.000 C 986.000,708.330 970.330,724.000 951.000,724.000 L 112.000,724.000 C 92.670,724.000 77.000,708.330 77.000,689.000 L 77.000,335.000 C 77.000,315.670 92.670,300.000 112.000,300.000 Z"/>
<Path StrokeThickness="1.0" Stroke="#ff000000" StrokeStartLineCap="Round" StrokeEndLineCap="Round" StrokeLineJoin="Round" Fill="#ff000000" Data=" M 25.000,462.500 L 50.000,462.500 C 52.760,462.500 55.000,464.740 55.000,467.500 L 55.000,557.500 C 55.000,560.260 52.760,562.500 50.000,562.500 L 25.000,562.500 C 22.240,562.500 20.000,560.260 20.000,557.500 L 20.000,467.500 C 20.000,464.740 22.240,462.500 25.000,462.500 Z"/>
</Canvas>
</Canvas>
</Viewbox>
<Rectangle x:Name="BatteryCharge" Canvas.Left="2" Canvas.Top="12" Fill="Green" Height="16" Width="35" Panel.ZIndex="1" />
<Canvas x:Name="PowerPlug" Panel.ZIndex="3" Canvas.Left="4" Canvas.Top="-3">
<Canvas.LayoutTransform>
<ScaleTransform ScaleX="2" ScaleY="2" />
</Canvas.LayoutTransform>
<Path Stroke="Black" StrokeStartLineCap="Round" Fill="Black" Data="M2.5,17.5 V10 Q5,10 5,6 H4 V4 H4 V6 H1 V4 H1 V6 H0 Q0,10 2.5,10" />
</Canvas>
<TextBlock x:Name="Warning" FontSize="36" FontWeight="ExtraBold" Foreground="Red" Canvas.Left="13" Canvas.Top="-7" Panel.ZIndex="3">!</TextBlock>
</Canvas>
</Viewbox>
</Button>
</Grid>
</UserControl>

View File

@@ -0,0 +1,155 @@
/*
* 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.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Threading;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.SystemComponents.Contracts.PowerSupply;
using SafeExamBrowser.UserInterface.Contracts.Shell;
namespace SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar
{
internal partial class PowerSupplyControl : UserControl, ISystemControl
{
private Brush initialBrush;
private bool infoShown, warningShown;
private double maxWidth;
private IPowerSupply powerSupply;
private IText text;
internal PowerSupplyControl(IPowerSupply powerSupply, IText text)
{
this.powerSupply = powerSupply;
this.text = text;
InitializeComponent();
InitializePowerSupplyControl();
}
public void Close()
{
Dispatcher.InvokeAsync(ClosePopup);
}
public void SetInformation(string text)
{
Dispatcher.InvokeAsync(() => PopupText.Text = text);
}
private void InitializePowerSupplyControl()
{
initialBrush = BatteryCharge.Fill;
maxWidth = BatteryCharge.Width;
powerSupply.StatusChanged += PowerSupply_StatusChanged;
Popup.CustomPopupPlacementCallback = new CustomPopupPlacementCallback(Popup_PlacementCallback);
UpdateStatus(powerSupply.GetStatus());
}
private void Button_Click(object sender, RoutedEventArgs e)
{
ClosePopup();
}
private CustomPopupPlacement[] Popup_PlacementCallback(Size popupSize, Size targetSize, Point offset)
{
return new[]
{
new CustomPopupPlacement(new Point(targetSize.Width / 2 - popupSize.Width / 2, -popupSize.Height), PopupPrimaryAxis.None)
};
}
private void PowerSupply_StatusChanged(IPowerSupplyStatus status)
{
Dispatcher.InvokeAsync(() => UpdateStatus(status));
}
private void UpdateStatus(IPowerSupplyStatus status)
{
var percentage = Math.Round(status.BatteryCharge * 100);
var tooltip = string.Empty;
RenderCharge(status.BatteryCharge, status.BatteryChargeStatus);
if (status.IsOnline)
{
infoShown = false;
warningShown = false;
tooltip = text.Get(percentage == 100 ? TextKey.SystemControl_BatteryCharged : TextKey.SystemControl_BatteryCharging);
tooltip = tooltip.Replace("%%CHARGE%%", percentage.ToString());
ClosePopup();
}
else
{
tooltip = text.Get(TextKey.SystemControl_BatteryRemainingCharge);
tooltip = tooltip.Replace("%%CHARGE%%", percentage.ToString());
tooltip = tooltip.Replace("%%HOURS%%", status.BatteryTimeRemaining.Hours.ToString());
tooltip = tooltip.Replace("%%MINUTES%%", status.BatteryTimeRemaining.Minutes.ToString());
HandleBatteryStatus(status.BatteryChargeStatus);
}
Button.ToolTip = tooltip;
PowerPlug.Visibility = status.IsOnline ? Visibility.Visible : Visibility.Collapsed;
Warning.Visibility = status.BatteryChargeStatus == BatteryChargeStatus.Critical ? Visibility.Visible : Visibility.Collapsed;
this.SetValue(System.Windows.Automation.AutomationProperties.NameProperty, tooltip);
}
private void RenderCharge(double charge, BatteryChargeStatus status)
{
var width = maxWidth * charge;
BatteryCharge.Width = width > maxWidth ? maxWidth : (width < 0 ? 0 : width);
switch (status)
{
case BatteryChargeStatus.Critical:
BatteryCharge.Fill = Brushes.Red;
break;
case BatteryChargeStatus.Low:
BatteryCharge.Fill = Brushes.Orange;
break;
default:
BatteryCharge.Fill = initialBrush;
break;
}
}
private void HandleBatteryStatus(BatteryChargeStatus chargeStatus)
{
if (chargeStatus == BatteryChargeStatus.Low && !infoShown)
{
ShowPopup(text.Get(TextKey.SystemControl_BatteryChargeLowInfo));
infoShown = true;
}
if (chargeStatus == BatteryChargeStatus.Critical && !warningShown)
{
ShowPopup(text.Get(TextKey.SystemControl_BatteryChargeCriticalWarning));
warningShown = true;
}
}
private void ShowPopup(string text)
{
Popup.IsOpen = true;
PopupText.Text = text;
Background = Brushes.LightGray;
}
private void ClosePopup()
{
Popup.IsOpen = false;
Background = Brushes.Transparent;
}
}
}

View File

@@ -0,0 +1,22 @@
<UserControl x:Class="SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar.QuitButton" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Mobile.Controls"
x:Name="root"
mc:Ignorable="d" d:DesignHeight="40" d:DesignWidth="40">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Templates/Buttons.xaml" />
<ResourceDictionary Source="../../Templates/Colors.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Button x:Name="Button" Click="Button_Click" Background="{StaticResource BackgroundBrush}" HorizontalAlignment="Stretch"
Template="{StaticResource TaskbarButton}" VerticalAlignment="Stretch"
AutomationProperties.Name="{Binding Path=(AutomationProperties.Name), ElementName=root}" />
</Grid>
</UserControl>

View File

@@ -0,0 +1,42 @@
/*
* 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 System.Windows.Controls;
using SafeExamBrowser.Core.Contracts.Resources.Icons;
using SafeExamBrowser.UserInterface.Contracts.Shell.Events;
using SafeExamBrowser.UserInterface.Shared.Utilities;
namespace SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar
{
internal partial class QuitButton : UserControl
{
internal event QuitButtonClickedEventHandler Clicked;
public QuitButton()
{
InitializeComponent();
LoadIcon();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Clicked?.Invoke(new CancelEventArgs());
}
private void LoadIcon()
{
var uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/ShutDown.xaml");
var resource = new XamlIconResource { Uri = uri };
Button.Content = IconResourceLoader.Load(resource);
}
}
}

View File

@@ -0,0 +1,35 @@
<UserControl x:Class="SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar.RaiseHandControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" d:DesignHeight="40" d:DesignWidth="40">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../../Templates/Buttons.xaml" />
<ResourceDictionary Source="../../Templates/Colors.xaml" />
<ResourceDictionary Source="../../Templates/ScrollViewers.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Popup x:Name="Popup" IsOpen="False" Placement="Custom" PlacementTarget="{Binding ElementName=Button}">
<Border Background="LightGray" BorderBrush="Gray" BorderThickness="1,1,1,0" >
<StackPanel>
<TextBox Name="Message" AcceptsReturn="True" Height="150" IsReadOnly="False" Margin="5,5,5,0" Width="350" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" />
<Grid>
<Button Name="HandButton" Background="Transparent" Height="30" Margin="5" Padding="5" Template="{StaticResource TaskbarButton}" Width="150">
<Viewbox Stretch="Uniform">
<TextBlock x:Name="HandButtonText" FontWeight="Bold" TextAlignment="Center" />
</Viewbox>
</Button>
</Grid>
</StackPanel>
</Border>
</Popup>
<Button x:Name="NotificationButton" Background="Transparent" Template="{StaticResource TaskbarButton}" Padding="5" Width="60">
<ContentControl Name="Icon" />
</Button>
</Grid>
</UserControl>

View File

@@ -0,0 +1,161 @@
/*
* 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.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using SafeExamBrowser.Core.Contracts.Resources.Icons;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Proctoring.Contracts;
using SafeExamBrowser.Settings.Proctoring;
using SafeExamBrowser.UserInterface.Contracts.Shell;
using SafeExamBrowser.UserInterface.Shared.Utilities;
namespace SafeExamBrowser.UserInterface.Mobile.Controls.Taskbar
{
public partial class RaiseHandControl : UserControl, INotificationControl
{
private readonly IProctoringController controller;
private readonly ProctoringSettings settings;
private readonly IText text;
private IconResource LoweredIcon;
private IconResource RaisedIcon;
public RaiseHandControl(IProctoringController controller, ProctoringSettings settings, IText text)
{
this.controller = controller;
this.settings = settings;
this.text = text;
InitializeComponent();
InitializeRaiseHandControl();
}
private void InitializeRaiseHandControl()
{
var originalBrush = NotificationButton.Background;
controller.HandLowered += () => Dispatcher.Invoke(ShowLowered);
controller.HandRaised += () => Dispatcher.Invoke(ShowRaised);
HandButton.Click += RaiseHandButton_Click;
HandButtonText.Text = text.Get(TextKey.Notification_ProctoringRaiseHand);
LoweredIcon = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/Hand_Lowered.xaml") };
RaisedIcon = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/Hand_Raised.xaml") };
Icon.Content = IconResourceLoader.Load(LoweredIcon);
var lastOpenedBySpacePress = false;
NotificationButton.PreviewKeyDown += (o, args) =>
{
if (args.Key == System.Windows.Input.Key.Space) // for some reason, the popup immediately closes again if opened by a Space Bar key event - as a mitigation, we record the space bar event and leave the popup open for at least 3 seconds
{
lastOpenedBySpacePress = true;
}
};
NotificationButton.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() =>
{
if (Popup.IsOpen && lastOpenedBySpacePress)
{
return;
}
Popup.IsOpen = Popup.IsMouseOver;
}));
NotificationButton.PreviewMouseLeftButtonUp += NotificationButton_PreviewMouseLeftButtonUp;
NotificationButton.PreviewMouseRightButtonUp += NotificationButton_PreviewMouseRightButtonUp;
NotificationButton.ToolTip = text.Get(TextKey.Notification_ProctoringHandLowered);
Popup.CustomPopupPlacementCallback = new CustomPopupPlacementCallback(Popup_PlacementCallback);
Popup.MouseLeave += (o, args) => Task.Delay(250).ContinueWith(_ => Dispatcher.Invoke(() =>
{
if (Popup.IsOpen && lastOpenedBySpacePress)
{
return;
}
Popup.IsOpen = IsMouseOver;
}));
Popup.Opened += (o, args) =>
{
Background = Brushes.LightGray;
NotificationButton.Background = Brushes.LightGray;
};
Popup.Closed += (o, args) =>
{
Background = originalBrush;
NotificationButton.Background = originalBrush;
lastOpenedBySpacePress = false;
};
}
private void NotificationButton_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (settings.ForceRaiseHandMessage || Popup.IsOpen)
{
Popup.IsOpen = !Popup.IsOpen;
}
else
{
ToggleHand();
}
}
private void NotificationButton_PreviewMouseRightButtonUp(object sender, MouseButtonEventArgs e)
{
Popup.IsOpen = !Popup.IsOpen;
}
private CustomPopupPlacement[] Popup_PlacementCallback(Size popupSize, Size targetSize, Point offset)
{
return new[]
{
new CustomPopupPlacement(new Point(targetSize.Width / 2 - popupSize.Width / 2, -popupSize.Height), PopupPrimaryAxis.None)
};
}
private void RaiseHandButton_Click(object sender, RoutedEventArgs e)
{
ToggleHand();
}
private void ToggleHand()
{
if (controller.IsHandRaised)
{
controller.LowerHand();
}
else
{
controller.RaiseHand(Message.Text);
Message.Clear();
}
}
private void ShowLowered()
{
HandButtonText.Text = text.Get(TextKey.Notification_ProctoringRaiseHand);
Icon.Content = IconResourceLoader.Load(LoweredIcon);
Message.IsEnabled = true;
NotificationButton.ToolTip = text.Get(TextKey.Notification_ProctoringHandLowered);
Popup.IsOpen = false;
}
private void ShowRaised()
{
HandButtonText.Text = text.Get(TextKey.Notification_ProctoringLowerHand);
Icon.Content = IconResourceLoader.Load(RaisedIcon);
Message.IsEnabled = false;
NotificationButton.ToolTip = text.Get(TextKey.Notification_ProctoringHandRaised);
}
}
}