SEBWIN-141: Implemented quit button and clock for action center.

This commit is contained in:
dbuechel 2019-03-15 11:38:59 +01:00
parent a47f68422c
commit b4ae1745fc
25 changed files with 331 additions and 102 deletions

View file

@ -12,6 +12,7 @@ using Moq;
using SafeExamBrowser.Client.Operations; using SafeExamBrowser.Client.Operations;
using SafeExamBrowser.Contracts.Client; using SafeExamBrowser.Contracts.Client;
using SafeExamBrowser.Contracts.Configuration.Settings; using SafeExamBrowser.Contracts.Configuration.Settings;
using SafeExamBrowser.Contracts.I18n;
using SafeExamBrowser.Contracts.Logging; using SafeExamBrowser.Contracts.Logging;
using SafeExamBrowser.Contracts.SystemComponents; using SafeExamBrowser.Contracts.SystemComponents;
using SafeExamBrowser.Contracts.UserInterface; using SafeExamBrowser.Contracts.UserInterface;
@ -25,18 +26,19 @@ namespace SafeExamBrowser.Client.UnitTests.Operations
private Mock<IActionCenter> actionCenter; private Mock<IActionCenter> actionCenter;
private Mock<IEnumerable<IActionCenterActivator>> activators; private Mock<IEnumerable<IActionCenterActivator>> activators;
private ActionCenterSettings actionCenterSettings; private ActionCenterSettings actionCenterSettings;
private Mock<ILogger> loggerMock; private Mock<ILogger> logger;
private TaskbarSettings taskbarSettings; private TaskbarSettings taskbarSettings;
private Mock<INotificationInfo> aboutInfoMock; private Mock<INotificationInfo> aboutInfo;
private Mock<INotificationController> aboutControllerMock; private Mock<INotificationController> aboutController;
private Mock<INotificationInfo> logInfoMock; private Mock<INotificationInfo> logInfo;
private Mock<INotificationController> logControllerMock; private Mock<INotificationController> logController;
private Mock<ISystemComponent<ISystemKeyboardLayoutControl>> keyboardLayoutMock; private Mock<ISystemComponent<ISystemKeyboardLayoutControl>> keyboardLayout;
private Mock<ISystemComponent<ISystemPowerSupplyControl>> powerSupplyMock; private Mock<ISystemComponent<ISystemPowerSupplyControl>> powerSupply;
private Mock<ISystemComponent<ISystemWirelessNetworkControl>> wirelessNetworkMock; private Mock<ISystemComponent<ISystemWirelessNetworkControl>> wirelessNetwork;
private Mock<ISystemInfo> systemInfoMock; private Mock<ISystemInfo> systemInfo;
private Mock<ITaskbar> taskbarMock; private Mock<ITaskbar> taskbar;
private Mock<IUserInterfaceFactory> uiFactoryMock; private Mock<IText> text;
private Mock<IUserInterfaceFactory> uiFactory;
private ShellOperation sut; private ShellOperation sut;
@ -46,42 +48,44 @@ namespace SafeExamBrowser.Client.UnitTests.Operations
actionCenter = new Mock<IActionCenter>(); actionCenter = new Mock<IActionCenter>();
activators = new Mock<IEnumerable<IActionCenterActivator>>(); activators = new Mock<IEnumerable<IActionCenterActivator>>();
actionCenterSettings = new ActionCenterSettings(); actionCenterSettings = new ActionCenterSettings();
loggerMock = new Mock<ILogger>(); logger = new Mock<ILogger>();
aboutInfoMock = new Mock<INotificationInfo>(); aboutInfo = new Mock<INotificationInfo>();
aboutControllerMock = new Mock<INotificationController>(); aboutController = new Mock<INotificationController>();
logInfoMock = new Mock<INotificationInfo>(); logInfo = new Mock<INotificationInfo>();
logControllerMock = new Mock<INotificationController>(); logController = new Mock<INotificationController>();
keyboardLayoutMock = new Mock<ISystemComponent<ISystemKeyboardLayoutControl>>(); keyboardLayout = new Mock<ISystemComponent<ISystemKeyboardLayoutControl>>();
powerSupplyMock = new Mock<ISystemComponent<ISystemPowerSupplyControl>>(); powerSupply = new Mock<ISystemComponent<ISystemPowerSupplyControl>>();
wirelessNetworkMock = new Mock<ISystemComponent<ISystemWirelessNetworkControl>>(); wirelessNetwork = new Mock<ISystemComponent<ISystemWirelessNetworkControl>>();
systemInfoMock = new Mock<ISystemInfo>(); systemInfo = new Mock<ISystemInfo>();
taskbarMock = new Mock<ITaskbar>(); taskbar = new Mock<ITaskbar>();
taskbarSettings = new TaskbarSettings(); taskbarSettings = new TaskbarSettings();
uiFactoryMock = new Mock<IUserInterfaceFactory>(); text = new Mock<IText>();
uiFactory = new Mock<IUserInterfaceFactory>();
taskbarSettings.ShowApplicationLog = true; taskbarSettings.ShowApplicationLog = true;
taskbarSettings.ShowKeyboardLayout = true; taskbarSettings.ShowKeyboardLayout = true;
taskbarSettings.ShowWirelessNetwork = true; taskbarSettings.ShowWirelessNetwork = true;
taskbarSettings.EnableTaskbar = true; taskbarSettings.EnableTaskbar = true;
systemInfoMock.SetupGet(s => s.HasBattery).Returns(true); systemInfo.SetupGet(s => s.HasBattery).Returns(true);
uiFactoryMock.Setup(u => u.CreateNotificationControl(It.IsAny<INotificationInfo>(), It.IsAny<Location>())).Returns(new Mock<INotificationControl>().Object); uiFactory.Setup(u => u.CreateNotificationControl(It.IsAny<INotificationInfo>(), It.IsAny<Location>())).Returns(new Mock<INotificationControl>().Object);
sut = new ShellOperation( sut = new ShellOperation(
actionCenter.Object, actionCenter.Object,
activators.Object, activators.Object,
actionCenterSettings, actionCenterSettings,
loggerMock.Object, logger.Object,
aboutInfoMock.Object, aboutInfo.Object,
aboutControllerMock.Object, aboutController.Object,
logInfoMock.Object, logInfo.Object,
logControllerMock.Object, logController.Object,
keyboardLayoutMock.Object, keyboardLayout.Object,
powerSupplyMock.Object, powerSupply.Object,
wirelessNetworkMock.Object, wirelessNetwork.Object,
systemInfoMock.Object, systemInfo.Object,
taskbarMock.Object, taskbar.Object,
taskbarSettings, taskbarSettings,
uiFactoryMock.Object); text.Object,
uiFactory.Object);
} }
[TestMethod] [TestMethod]
@ -89,11 +93,11 @@ namespace SafeExamBrowser.Client.UnitTests.Operations
{ {
sut.Perform(); sut.Perform();
keyboardLayoutMock.Verify(k => k.Initialize(), Times.Once); keyboardLayout.Verify(k => k.Initialize(), Times.Once);
powerSupplyMock.Verify(p => p.Initialize(), Times.Once); powerSupply.Verify(p => p.Initialize(), Times.Once);
wirelessNetworkMock.Verify(w => w.Initialize(), Times.Once); wirelessNetwork.Verify(w => w.Initialize(), Times.Once);
taskbarMock.Verify(t => t.AddSystemControl(It.IsAny<ISystemControl>()), Times.Exactly(3)); taskbar.Verify(t => t.AddSystemControl(It.IsAny<ISystemControl>()), Times.Exactly(3));
taskbarMock.Verify(t => t.AddNotificationControl(It.IsAny<INotificationControl>()), Times.Exactly(2)); taskbar.Verify(t => t.AddNotificationControl(It.IsAny<INotificationControl>()), Times.Exactly(2));
} }
[TestMethod] [TestMethod]
@ -101,10 +105,10 @@ namespace SafeExamBrowser.Client.UnitTests.Operations
{ {
sut.Revert(); sut.Revert();
aboutControllerMock.Verify(c => c.Terminate(), Times.Once); aboutController.Verify(c => c.Terminate(), Times.Once);
keyboardLayoutMock.Verify(k => k.Terminate(), Times.Once); keyboardLayout.Verify(k => k.Terminate(), Times.Once);
powerSupplyMock.Verify(p => p.Terminate(), Times.Once); powerSupply.Verify(p => p.Terminate(), Times.Once);
wirelessNetworkMock.Verify(w => w.Terminate(), Times.Once); wirelessNetwork.Verify(w => w.Terminate(), Times.Once);
} }
} }
} }

View file

@ -168,6 +168,7 @@ namespace SafeExamBrowser.Client
private void RegisterEvents() private void RegisterEvents()
{ {
actionCenter.QuitButtonClicked += Shell_QuitButtonClicked;
Browser.ConfigurationDownloadRequested += Browser_ConfigurationDownloadRequested; Browser.ConfigurationDownloadRequested += Browser_ConfigurationDownloadRequested;
ClientHost.MessageBoxRequested += ClientHost_MessageBoxRequested; ClientHost.MessageBoxRequested += ClientHost_MessageBoxRequested;
ClientHost.PasswordRequested += ClientHost_PasswordRequested; ClientHost.PasswordRequested += ClientHost_PasswordRequested;
@ -176,16 +177,17 @@ namespace SafeExamBrowser.Client
displayMonitor.DisplayChanged += DisplayMonitor_DisplaySettingsChanged; displayMonitor.DisplayChanged += DisplayMonitor_DisplaySettingsChanged;
processMonitor.ExplorerStarted += ProcessMonitor_ExplorerStarted; processMonitor.ExplorerStarted += ProcessMonitor_ExplorerStarted;
runtime.ConnectionLost += Runtime_ConnectionLost; runtime.ConnectionLost += Runtime_ConnectionLost;
taskbar.QuitButtonClicked += Taskbar_QuitButtonClicked; taskbar.QuitButtonClicked += Shell_QuitButtonClicked;
windowMonitor.WindowChanged += WindowMonitor_WindowChanged; windowMonitor.WindowChanged += WindowMonitor_WindowChanged;
} }
private void DeregisterEvents() private void DeregisterEvents()
{ {
actionCenter.QuitButtonClicked -= Shell_QuitButtonClicked;
displayMonitor.DisplayChanged -= DisplayMonitor_DisplaySettingsChanged; displayMonitor.DisplayChanged -= DisplayMonitor_DisplaySettingsChanged;
processMonitor.ExplorerStarted -= ProcessMonitor_ExplorerStarted; processMonitor.ExplorerStarted -= ProcessMonitor_ExplorerStarted;
runtime.ConnectionLost -= Runtime_ConnectionLost; runtime.ConnectionLost -= Runtime_ConnectionLost;
taskbar.QuitButtonClicked -= Taskbar_QuitButtonClicked; taskbar.QuitButtonClicked -= Shell_QuitButtonClicked;
windowMonitor.WindowChanged -= WindowMonitor_WindowChanged; windowMonitor.WindowChanged -= WindowMonitor_WindowChanged;
if (Browser != null) if (Browser != null)
@ -216,26 +218,6 @@ namespace SafeExamBrowser.Client
Browser.Start(); Browser.Start();
} }
private void DisplayMonitor_DisplaySettingsChanged()
{
logger.Info("Reinitializing working area...");
displayMonitor.InitializePrimaryDisplay(taskbar.GetAbsoluteHeight());
logger.Info("Reinitializing taskbar bounds...");
taskbar.InitializeBounds();
logger.Info("Desktop successfully restored.");
}
private void ProcessMonitor_ExplorerStarted()
{
logger.Info("Trying to terminate Windows explorer...");
explorerShell.Terminate();
logger.Info("Reinitializing working area...");
displayMonitor.InitializePrimaryDisplay(taskbar.GetAbsoluteHeight());
logger.Info("Reinitializing taskbar bounds...");
taskbar.InitializeBounds();
logger.Info("Desktop successfully restored.");
}
private void Browser_ConfigurationDownloadRequested(string fileName, DownloadEventArgs args) private void Browser_ConfigurationDownloadRequested(string fileName, DownloadEventArgs args)
{ {
if (Settings.ConfigurationMode == ConfigurationMode.ConfigureClient) if (Settings.ConfigurationMode == ConfigurationMode.ConfigureClient)
@ -337,6 +319,15 @@ namespace SafeExamBrowser.Client
shutdown.Invoke(); shutdown.Invoke();
} }
private void DisplayMonitor_DisplaySettingsChanged()
{
logger.Info("Reinitializing working area...");
displayMonitor.InitializePrimaryDisplay(taskbar.GetAbsoluteHeight());
logger.Info("Reinitializing taskbar bounds...");
taskbar.InitializeBounds();
logger.Info("Desktop successfully restored.");
}
private void Operations_ProgressChanged(ProgressChangedEventArgs args) private void Operations_ProgressChanged(ProgressChangedEventArgs args)
{ {
if (args.CurrentValue.HasValue) if (args.CurrentValue.HasValue)
@ -370,6 +361,17 @@ namespace SafeExamBrowser.Client
splashScreen?.UpdateStatus(status, true); splashScreen?.UpdateStatus(status, true);
} }
private void ProcessMonitor_ExplorerStarted()
{
logger.Info("Trying to terminate Windows explorer...");
explorerShell.Terminate();
logger.Info("Reinitializing working area...");
displayMonitor.InitializePrimaryDisplay(taskbar.GetAbsoluteHeight());
logger.Info("Reinitializing taskbar bounds...");
taskbar.InitializeBounds();
logger.Info("Desktop successfully restored.");
}
private void Runtime_ConnectionLost() private void Runtime_ConnectionLost()
{ {
logger.Error("Lost connection to the runtime!"); logger.Error("Lost connection to the runtime!");
@ -378,7 +380,7 @@ namespace SafeExamBrowser.Client
shutdown.Invoke(); shutdown.Invoke();
} }
private void Taskbar_QuitButtonClicked(System.ComponentModel.CancelEventArgs args) private void Shell_QuitButtonClicked(System.ComponentModel.CancelEventArgs args)
{ {
var hasQuitPassword = !String.IsNullOrEmpty(Settings.QuitPasswordHash); var hasQuitPassword = !String.IsNullOrEmpty(Settings.QuitPasswordHash);
var requestShutdown = false; var requestShutdown = false;

View file

@ -94,7 +94,7 @@ namespace SafeExamBrowser.Client
processMonitor = new ProcessMonitor(new ModuleLogger(logger, nameof(ProcessMonitor)), nativeMethods); processMonitor = new ProcessMonitor(new ModuleLogger(logger, nameof(ProcessMonitor)), nativeMethods);
uiFactory = new UserInterfaceFactory(text); uiFactory = new UserInterfaceFactory(text);
runtimeProxy = new RuntimeProxy(runtimeHostUri, new ProxyObjectFactory(), new ModuleLogger(logger, nameof(RuntimeProxy))); runtimeProxy = new RuntimeProxy(runtimeHostUri, new ProxyObjectFactory(), new ModuleLogger(logger, nameof(RuntimeProxy)));
taskbar = new Taskbar(new ModuleLogger(logger, nameof(taskbar))); taskbar = new Taskbar(new ModuleLogger(logger, nameof(Taskbar)));
windowMonitor = new WindowMonitor(new ModuleLogger(logger, nameof(WindowMonitor)), nativeMethods); windowMonitor = new WindowMonitor(new ModuleLogger(logger, nameof(WindowMonitor)), nativeMethods);
wirelessNetwork = new WirelessNetwork(new ModuleLogger(logger, nameof(WirelessNetwork)), text); wirelessNetwork = new WirelessNetwork(new ModuleLogger(logger, nameof(WirelessNetwork)), text);
@ -276,6 +276,7 @@ namespace SafeExamBrowser.Client
systemInfo, systemInfo,
taskbar, taskbar,
configuration.Settings.Taskbar, configuration.Settings.Taskbar,
text,
uiFactory); uiFactory);
return operation; return operation;

View file

@ -35,6 +35,7 @@ namespace SafeExamBrowser.Client.Operations
private ISystemInfo systemInfo; private ISystemInfo systemInfo;
private ITaskbar taskbar; private ITaskbar taskbar;
private TaskbarSettings taskbarSettings; private TaskbarSettings taskbarSettings;
private IText text;
private IUserInterfaceFactory uiFactory; private IUserInterfaceFactory uiFactory;
public event ActionRequiredEventHandler ActionRequired { add { } remove { } } public event ActionRequiredEventHandler ActionRequired { add { } remove { } }
@ -55,6 +56,7 @@ namespace SafeExamBrowser.Client.Operations
ISystemInfo systemInfo, ISystemInfo systemInfo,
ITaskbar taskbar, ITaskbar taskbar,
TaskbarSettings taskbarSettings, TaskbarSettings taskbarSettings,
IText text,
IUserInterfaceFactory uiFactory) IUserInterfaceFactory uiFactory)
{ {
this.aboutInfo = aboutInfo; this.aboutInfo = aboutInfo;
@ -67,8 +69,9 @@ namespace SafeExamBrowser.Client.Operations
this.logController = logController; this.logController = logController;
this.keyboardLayout = keyboardLayout; this.keyboardLayout = keyboardLayout;
this.powerSupply = powerSupply; this.powerSupply = powerSupply;
this.taskbarSettings = taskbarSettings;
this.systemInfo = systemInfo; this.systemInfo = systemInfo;
this.taskbarSettings = taskbarSettings;
this.text = text;
this.taskbar = taskbar; this.taskbar = taskbar;
this.uiFactory = uiFactory; this.uiFactory = uiFactory;
this.wirelessNetwork = wirelessNetwork; this.wirelessNetwork = wirelessNetwork;
@ -102,6 +105,7 @@ namespace SafeExamBrowser.Client.Operations
if (actionCenterSettings.EnableActionCenter) if (actionCenterSettings.EnableActionCenter)
{ {
logger.Info("Initializing action center..."); logger.Info("Initializing action center...");
actionCenter.InitializeText(text);
InitializeAboutNotificationForActionCenter(); InitializeAboutNotificationForActionCenter();
InitializeClockForActionCenter(); InitializeClockForActionCenter();
@ -127,6 +131,7 @@ namespace SafeExamBrowser.Client.Operations
if (taskbarSettings.EnableTaskbar) if (taskbarSettings.EnableTaskbar)
{ {
logger.Info("Initializing taskbar..."); logger.Info("Initializing taskbar...");
taskbar.InitializeText(text);
InitializeAboutNotificationForTaskbar(); InitializeAboutNotificationForTaskbar();
InitializeClockForTaskbar(); InitializeClockForTaskbar();
@ -166,7 +171,7 @@ namespace SafeExamBrowser.Client.Operations
private void InitializeClockForActionCenter() private void InitializeClockForActionCenter()
{ {
//TODO: actionCenter.ShowClock = settings.ShowClock; actionCenter.ShowClock = actionCenterSettings.ShowClock;
} }
private void InitializeClockForTaskbar() private void InitializeClockForTaskbar()

View file

@ -89,6 +89,10 @@ namespace SafeExamBrowser.Configuration.ConfigurationData
var settings = new Settings(); var settings = new Settings();
settings.ActionCenter.EnableActionCenter = true; settings.ActionCenter.EnableActionCenter = true;
settings.ActionCenter.ShowApplicationLog = false;
settings.ActionCenter.ShowKeyboardLayout = true;
settings.ActionCenter.ShowWirelessNetwork = false;
settings.ActionCenter.ShowClock = true;
settings.Browser.StartUrl = "https://www.safeexambrowser.org/start"; settings.Browser.StartUrl = "https://www.safeexambrowser.org/start";
settings.Browser.AllowConfigurationDownloads = true; settings.Browser.AllowConfigurationDownloads = true;

View file

@ -25,6 +25,11 @@ namespace SafeExamBrowser.Contracts.Configuration.Settings
/// Determines whether the application log is accessible via the action center. /// Determines whether the application log is accessible via the action center.
/// </summary> /// </summary>
public bool ShowApplicationLog { get; set; } public bool ShowApplicationLog { get; set; }
/// <summary>
/// Determines whether the current date and time will be rendered in the action center.
/// </summary>
public bool ShowClock { get; set; }
/// <summary> /// <summary>
/// Determines whether the system control for the keyboard layout is accessible via the action center. /// Determines whether the system control for the keyboard layout is accessible via the action center.

View file

@ -96,6 +96,7 @@ namespace SafeExamBrowser.Contracts.I18n
PasswordDialog_SettingsPasswordRequired, PasswordDialog_SettingsPasswordRequired,
PasswordDialog_SettingsPasswordRequiredTitle, PasswordDialog_SettingsPasswordRequiredTitle,
RuntimeWindow_ApplicationRunning, RuntimeWindow_ApplicationRunning,
Shell_QuitButton,
SystemControl_BatteryCharged, SystemControl_BatteryCharged,
SystemControl_BatteryCharging, SystemControl_BatteryCharging,
SystemControl_BatteryChargeCriticalWarning, SystemControl_BatteryChargeCriticalWarning,

View file

@ -6,6 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
using SafeExamBrowser.Contracts.I18n;
using SafeExamBrowser.Contracts.UserInterface.Shell.Events; using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
namespace SafeExamBrowser.Contracts.UserInterface.Shell namespace SafeExamBrowser.Contracts.UserInterface.Shell
@ -15,6 +16,11 @@ namespace SafeExamBrowser.Contracts.UserInterface.Shell
/// </summary> /// </summary>
public interface IActionCenter public interface IActionCenter
{ {
/// <summary>
/// Controls the visibility of the clock.
/// </summary>
bool ShowClock { set; }
/// <summary> /// <summary>
/// Event fired when the user clicked the quit button. /// Event fired when the user clicked the quit button.
/// </summary> /// </summary>
@ -44,6 +50,11 @@ namespace SafeExamBrowser.Contracts.UserInterface.Shell
/// Makes the action center invisible. /// Makes the action center invisible.
/// </summary> /// </summary>
void Hide(); void Hide();
/// <summary>
/// Initializes all text elements in the action center.
/// </summary>
void InitializeText(IText text);
/// <summary> /// <summary>
/// Registers the specified activator to control the visibility of the action center. /// Registers the specified activator to control the visibility of the action center.

View file

@ -6,6 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
using SafeExamBrowser.Contracts.I18n;
using SafeExamBrowser.Contracts.UserInterface.Shell.Events; using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
namespace SafeExamBrowser.Contracts.UserInterface.Shell namespace SafeExamBrowser.Contracts.UserInterface.Shell
@ -55,6 +56,11 @@ namespace SafeExamBrowser.Contracts.UserInterface.Shell
/// </summary> /// </summary>
void InitializeBounds(); void InitializeBounds();
/// <summary>
/// Initializes all text elements in the taskbar.
/// </summary>
void InitializeText(IText text);
/// <summary> /// <summary>
/// Shows the taskbar. /// Shows the taskbar.
/// </summary> /// </summary>

View file

@ -112,7 +112,7 @@
Configuration Error Configuration Error
</Entry> </Entry>
<Entry key="Notification_AboutTooltip"> <Entry key="Notification_AboutTooltip">
About Safe Exam Browser Information about SEB
</Entry> </Entry>
<Entry key="Notification_LogTooltip"> <Entry key="Notification_LogTooltip">
Application Log Application Log
@ -246,6 +246,9 @@
<Entry key="RuntimeWindow_ApplicationRunning"> <Entry key="RuntimeWindow_ApplicationRunning">
The application is running. The application is running.
</Entry> </Entry>
<Entry key="Shell_QuitButton">
Terminate Session
</Entry>
<Entry key="SystemControl_BatteryCharging"> <Entry key="SystemControl_BatteryCharging">
Plugged in, charging... (%%CHARGE%%%) Plugged in, charging... (%%CHARGE%%%)
</Entry> </Entry>

View file

@ -3,7 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Desktop" xmlns:local="clr-namespace:SafeExamBrowser.UserInterface.Desktop.Controls"
mc:Ignorable="d" Title="ActionCenter" Height="1000" Width="400" Background="#EEF0F0F0" AllowsTransparency="True" WindowStyle="None" Topmost="True" ResizeMode="NoResize"> mc:Ignorable="d" Title="ActionCenter" Height="1000" Width="400" Background="#EEF0F0F0" AllowsTransparency="True" WindowStyle="None" Topmost="True" ResizeMode="NoResize">
<Window.Resources> <Window.Resources>
<ResourceDictionary> <ResourceDictionary>
@ -20,6 +20,9 @@
<ScrollViewer Grid.Row="0" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" Template="{StaticResource SmallBarScrollViewer}"> <ScrollViewer Grid.Row="0" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" Template="{StaticResource SmallBarScrollViewer}">
<StackPanel x:Name="ApplicationPanel" Orientation="Vertical" /> <StackPanel x:Name="ApplicationPanel" Orientation="Vertical" />
</ScrollViewer> </ScrollViewer>
<UniformGrid x:Name="ControlPanel" Grid.Row="1" Columns="4" Margin="10" /> <UniformGrid x:Name="ControlPanel" Grid.Row="1" Columns="4" Margin="10">
<local:ActionCenterClock x:Name="Clock" />
<local:ActionCenterQuitButton x:Name="QuitButton" />
</UniformGrid>
</Grid> </Grid>
</Window> </Window>

View file

@ -9,6 +9,7 @@
using System; using System;
using System.Windows; using System.Windows;
using System.Windows.Media.Animation; using System.Windows.Media.Animation;
using SafeExamBrowser.Contracts.I18n;
using SafeExamBrowser.Contracts.UserInterface.Shell; using SafeExamBrowser.Contracts.UserInterface.Shell;
using SafeExamBrowser.Contracts.UserInterface.Shell.Events; using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
@ -16,11 +17,17 @@ namespace SafeExamBrowser.UserInterface.Desktop
{ {
public partial class ActionCenter : Window, IActionCenter public partial class ActionCenter : Window, IActionCenter
{ {
public bool ShowClock
{
set { Dispatcher.Invoke(() => Clock.Visibility = value ? Visibility.Visible : Visibility.Collapsed); }
}
public event QuitButtonClickedEventHandler QuitButtonClicked; public event QuitButtonClickedEventHandler QuitButtonClicked;
public ActionCenter() public ActionCenter()
{ {
InitializeComponent(); InitializeComponent();
InitializeActionCenter();
} }
public void AddApplicationControl(IApplicationControl control) public void AddApplicationControl(IApplicationControl control)
@ -35,7 +42,7 @@ namespace SafeExamBrowser.UserInterface.Desktop
{ {
if (control is UIElement uiElement) if (control is UIElement uiElement)
{ {
ControlPanel.Children.Add(uiElement); ControlPanel.Children.Insert(ControlPanel.Children.Count - 2, uiElement);
} }
} }
@ -43,7 +50,7 @@ namespace SafeExamBrowser.UserInterface.Desktop
{ {
if (control is UIElement uiElement) if (control is UIElement uiElement)
{ {
ControlPanel.Children.Add(uiElement); ControlPanel.Children.Insert(ControlPanel.Children.Count - 2, uiElement);
} }
} }
@ -57,6 +64,12 @@ namespace SafeExamBrowser.UserInterface.Desktop
Dispatcher.Invoke(HideAnimated); Dispatcher.Invoke(HideAnimated);
} }
public void InitializeText(IText text)
{
QuitButton.ToolTip = text.Get(TextKey.Shell_QuitButton);
QuitButton.Text.Text = text.Get(TextKey.Shell_QuitButton);
}
public void Register(IActionCenterActivator activator) public void Register(IActionCenterActivator activator)
{ {
activator.Activate += Activator_Activate; activator.Activate += Activator_Activate;
@ -170,5 +183,10 @@ namespace SafeExamBrowser.UserInterface.Desktop
} }
}); });
} }
private void InitializeActionCenter()
{
QuitButton.Clicked += (args) => QuitButtonClicked?.Invoke(args);
}
} }
} }

View file

@ -0,0 +1,36 @@
<UserControl x:Class="SafeExamBrowser.UserInterface.Desktop.Controls.ActionCenterClock"
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.Desktop.Controls"
mc:Ignorable="d" d:DesignHeight="100" d:DesignWidth="125">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Templates/Buttons.xaml" />
<ResourceDictionary Source="../Templates/Colors.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid Background="{StaticResource ActionCenterDarkBrush}" Height="64" Margin="2" ToolTip="{Binding Path=ToolTip}">
<Button x:Name="Button" IsEnabled="False" Padding="2" Template="{StaticResource ActionCenterButton}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<RowDefinition Height="3*" />
</Grid.RowDefinitions>
<fa:ImageAwesome Grid.Row="0" Foreground="Black" Icon="ClockOutline" Margin="2" VerticalAlignment="Center" />
<Grid Grid.Row="1" Background="Transparent" Margin="5,0" VerticalAlignment="Bottom">
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<TextBlock x:Name="TimeTextBlock" Grid.Row="0" Text="{Binding Path=Time}" FontSize="11" Foreground="White" HorizontalAlignment="Center" />
<TextBlock x:Name="DateTextBlock" Grid.Row="1" Text="{Binding Path=Date}" FontSize="11" Foreground="White" HorizontalAlignment="Center" />
</Grid>
</Grid>
</Button>
</Grid>
</UserControl>

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Windows.Controls;
using SafeExamBrowser.UserInterface.Desktop.ViewModels;
namespace SafeExamBrowser.UserInterface.Desktop.Controls
{
public partial class ActionCenterClock : UserControl
{
private DateTimeViewModel model;
public ActionCenterClock()
{
InitializeComponent();
InitializeControl();
}
private void InitializeControl()
{
model = new DateTimeViewModel(true);
DataContext = model;
TimeTextBlock.DataContext = model;
DateTextBlock.DataContext = model;
}
}
}

View file

@ -20,7 +20,7 @@
<RowDefinition Height="2*" /> <RowDefinition Height="2*" />
<RowDefinition Height="3*" /> <RowDefinition Height="3*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<ContentControl Grid.Row="0" x:Name="Icon" Foreground="White" VerticalAlignment="Center" /> <ContentControl Grid.Row="0" x:Name="Icon" VerticalAlignment="Center" />
<TextBlock Grid.Row="1" x:Name="Text" FontSize="11" Foreground="White" TextAlignment="Center" TextTrimming="CharacterEllipsis" TextWrapping="Wrap" VerticalAlignment="Bottom" /> <TextBlock Grid.Row="1" x:Name="Text" FontSize="11" Foreground="White" TextAlignment="Center" TextTrimming="CharacterEllipsis" TextWrapping="Wrap" VerticalAlignment="Bottom" />
</Grid> </Grid>
</Button> </Button>

View file

@ -0,0 +1,28 @@
<UserControl x:Class="SafeExamBrowser.UserInterface.Desktop.Controls.ActionCenterQuitButton"
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.Desktop.Controls"
mc:Ignorable="d" d:DesignHeight="100" d:DesignWidth="125">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Templates/Buttons.xaml" />
<ResourceDictionary Source="../Templates/Colors.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid Background="{StaticResource ActionCenterDarkBrush}" Height="64" Margin="2">
<Button x:Name="Button" Padding="2" Template="{StaticResource ActionCenterButton}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<RowDefinition Height="3*" />
</Grid.RowDefinitions>
<ContentControl Grid.Row="0" x:Name="Icon" Foreground="Black" VerticalAlignment="Center" />
<TextBlock Grid.Row="1" x:Name="Text" FontSize="11" Foreground="White" TextAlignment="Center" TextTrimming="CharacterEllipsis" TextWrapping="Wrap" VerticalAlignment="Bottom" />
</Grid>
</Button>
</Grid>
</UserControl>

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.ComponentModel;
using System.Windows.Controls;
using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
using SafeExamBrowser.UserInterface.Desktop.Utilities;
namespace SafeExamBrowser.UserInterface.Desktop.Controls
{
public partial class ActionCenterQuitButton : UserControl
{
public event QuitButtonClickedEventHandler Clicked;
public ActionCenterQuitButton()
{
InitializeComponent();
InitializeControl();
}
private void InitializeControl()
{
var uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/ShutDown.xaml");
var resource = new XamlIconResource(uri);
Icon.Content = IconResourceLoader.Load(resource);
Button.Click += (o, args) => Clicked?.Invoke(new CancelEventArgs());
}
}
}

View file

@ -1,4 +1,4 @@
<UserControl x:Class="SafeExamBrowser.UserInterface.Desktop.Controls.DateTimeControl" <UserControl x:Class="SafeExamBrowser.UserInterface.Desktop.Controls.TaskbarClock"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

View file

@ -11,14 +11,19 @@ using SafeExamBrowser.UserInterface.Desktop.ViewModels;
namespace SafeExamBrowser.UserInterface.Desktop.Controls namespace SafeExamBrowser.UserInterface.Desktop.Controls
{ {
public partial class DateTimeControl : UserControl public partial class TaskbarClock : UserControl
{ {
private DateTimeViewModel model = new DateTimeViewModel(); private DateTimeViewModel model;
public DateTimeControl() public TaskbarClock()
{ {
InitializeComponent(); InitializeComponent();
InitializeControl();
}
private void InitializeControl()
{
model = new DateTimeViewModel(false);
DataContext = model; DataContext = model;
TimeTextBlock.DataContext = model; TimeTextBlock.DataContext = model;
DateTextBlock.DataContext = model; DateTextBlock.DataContext = model;

View file

@ -1,4 +1,4 @@
<UserControl x:Class="SafeExamBrowser.UserInterface.Desktop.Controls.QuitButton" <UserControl x:Class="SafeExamBrowser.UserInterface.Desktop.Controls.TaskbarQuitButton"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

View file

@ -15,11 +15,11 @@ using SafeExamBrowser.UserInterface.Desktop.Utilities;
namespace SafeExamBrowser.UserInterface.Desktop.Controls namespace SafeExamBrowser.UserInterface.Desktop.Controls
{ {
public partial class QuitButton : UserControl public partial class TaskbarQuitButton : UserControl
{ {
public event QuitButtonClickedEventHandler Clicked; public event QuitButtonClickedEventHandler Clicked;
public QuitButton() public TaskbarQuitButton()
{ {
InitializeComponent(); InitializeComponent();
LoadIcon(); LoadIcon();

View file

@ -80,6 +80,9 @@
<Compile Include="Controls\ActionCenterApplicationButton.xaml.cs"> <Compile Include="Controls\ActionCenterApplicationButton.xaml.cs">
<DependentUpon>ActionCenterApplicationButton.xaml</DependentUpon> <DependentUpon>ActionCenterApplicationButton.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="Controls\ActionCenterClock.xaml.cs">
<DependentUpon>ActionCenterClock.xaml</DependentUpon>
</Compile>
<Compile Include="Controls\ActionCenterKeyboardLayoutButton.xaml.cs"> <Compile Include="Controls\ActionCenterKeyboardLayoutButton.xaml.cs">
<DependentUpon>ActionCenterKeyboardLayoutButton.xaml</DependentUpon> <DependentUpon>ActionCenterKeyboardLayoutButton.xaml</DependentUpon>
</Compile> </Compile>
@ -92,6 +95,9 @@
<Compile Include="Controls\ActionCenterPowerSupplyControl.xaml.cs"> <Compile Include="Controls\ActionCenterPowerSupplyControl.xaml.cs">
<DependentUpon>ActionCenterPowerSupplyControl.xaml</DependentUpon> <DependentUpon>ActionCenterPowerSupplyControl.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="Controls\ActionCenterQuitButton.xaml.cs">
<DependentUpon>ActionCenterQuitButton.xaml</DependentUpon>
</Compile>
<Compile Include="Controls\ActionCenterWirelessNetworkButton.xaml.cs"> <Compile Include="Controls\ActionCenterWirelessNetworkButton.xaml.cs">
<DependentUpon>ActionCenterWirelessNetworkButton.xaml</DependentUpon> <DependentUpon>ActionCenterWirelessNetworkButton.xaml</DependentUpon>
</Compile> </Compile>
@ -104,8 +110,8 @@
<Compile Include="Controls\TaskbarApplicationInstanceButton.xaml.cs"> <Compile Include="Controls\TaskbarApplicationInstanceButton.xaml.cs">
<DependentUpon>TaskbarApplicationInstanceButton.xaml</DependentUpon> <DependentUpon>TaskbarApplicationInstanceButton.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="Controls\DateTimeControl.xaml.cs"> <Compile Include="Controls\TaskbarClock.xaml.cs">
<DependentUpon>DateTimeControl.xaml</DependentUpon> <DependentUpon>TaskbarClock.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="Controls\TaskbarKeyboardLayoutButton.xaml.cs"> <Compile Include="Controls\TaskbarKeyboardLayoutButton.xaml.cs">
<DependentUpon>TaskbarKeyboardLayoutButton.xaml</DependentUpon> <DependentUpon>TaskbarKeyboardLayoutButton.xaml</DependentUpon>
@ -119,8 +125,8 @@
<Compile Include="Controls\TaskbarPowerSupplyControl.xaml.cs"> <Compile Include="Controls\TaskbarPowerSupplyControl.xaml.cs">
<DependentUpon>TaskbarPowerSupplyControl.xaml</DependentUpon> <DependentUpon>TaskbarPowerSupplyControl.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="Controls\QuitButton.xaml.cs"> <Compile Include="Controls\TaskbarQuitButton.xaml.cs">
<DependentUpon>QuitButton.xaml</DependentUpon> <DependentUpon>TaskbarQuitButton.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="Controls\TaskbarWirelessNetworkButton.xaml.cs"> <Compile Include="Controls\TaskbarWirelessNetworkButton.xaml.cs">
<DependentUpon>TaskbarWirelessNetworkButton.xaml</DependentUpon> <DependentUpon>TaskbarWirelessNetworkButton.xaml</DependentUpon>
@ -169,6 +175,10 @@
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
<Page Include="Controls\ActionCenterClock.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Controls\ActionCenterKeyboardLayoutButton.xaml"> <Page Include="Controls\ActionCenterKeyboardLayoutButton.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
@ -185,6 +195,10 @@
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
<Page Include="Controls\ActionCenterQuitButton.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Controls\ActionCenterWirelessNetworkButton.xaml"> <Page Include="Controls\ActionCenterWirelessNetworkButton.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
@ -201,7 +215,7 @@
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
<Page Include="Controls\DateTimeControl.xaml"> <Page Include="Controls\TaskbarClock.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
@ -221,7 +235,7 @@
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType> <SubType>Designer</SubType>
</Page> </Page>
<Page Include="Controls\QuitButton.xaml"> <Page Include="Controls\TaskbarQuitButton.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>

View file

@ -26,7 +26,7 @@
</ScrollViewer> </ScrollViewer>
<StackPanel Grid.Column="1" x:Name="NotificationStackPanel" Orientation="Horizontal" VerticalAlignment="Stretch" /> <StackPanel Grid.Column="1" x:Name="NotificationStackPanel" Orientation="Horizontal" VerticalAlignment="Stretch" />
<StackPanel Grid.Column="2" x:Name="SystemControlStackPanel" Orientation="Horizontal" VerticalAlignment="Stretch" /> <StackPanel Grid.Column="2" x:Name="SystemControlStackPanel" Orientation="Horizontal" VerticalAlignment="Stretch" />
<local:DateTimeControl Grid.Column="3" x:Name="Clock" Foreground="DimGray" Padding="10,0,10,0" /> <local:TaskbarClock Grid.Column="3" x:Name="Clock" Foreground="DimGray" Padding="10,0,10,0" />
<local:QuitButton Grid.Column="4" x:Name="QuitButton" /> <local:TaskbarQuitButton Grid.Column="4" x:Name="QuitButton" />
</Grid> </Grid>
</Window> </Window>

View file

@ -8,6 +8,7 @@
using System.ComponentModel; using System.ComponentModel;
using System.Windows; using System.Windows;
using SafeExamBrowser.Contracts.I18n;
using SafeExamBrowser.Contracts.Logging; using SafeExamBrowser.Contracts.Logging;
using SafeExamBrowser.Contracts.UserInterface.Shell; using SafeExamBrowser.Contracts.UserInterface.Shell;
using SafeExamBrowser.Contracts.UserInterface.Shell.Events; using SafeExamBrowser.Contracts.UserInterface.Shell.Events;
@ -29,13 +30,10 @@ namespace SafeExamBrowser.UserInterface.Desktop
public Taskbar(ILogger logger) public Taskbar(ILogger logger)
{ {
InitializeComponent();
this.logger = logger; this.logger = logger;
Closing += Taskbar_Closing; InitializeComponent();
Loaded += (o, args) => InitializeBounds(); InitializeTaskbar();
QuitButton.Clicked += QuitButton_Clicked;
} }
public void AddApplicationControl(IApplicationControl control) public void AddApplicationControl(IApplicationControl control)
@ -94,6 +92,14 @@ namespace SafeExamBrowser.UserInterface.Desktop
}); });
} }
public void InitializeText(IText text)
{
Dispatcher.Invoke(() =>
{
QuitButton.ToolTip = text.Get(TextKey.Shell_QuitButton);
});
}
public new void Show() public new void Show()
{ {
Dispatcher.Invoke(base.Show); Dispatcher.Invoke(base.Show);
@ -122,5 +128,12 @@ namespace SafeExamBrowser.UserInterface.Desktop
} }
} }
} }
private void InitializeTaskbar()
{
Closing += Taskbar_Closing;
Loaded += (o, args) => InitializeBounds();
QuitButton.Clicked += QuitButton_Clicked;
}
} }
} }

View file

@ -12,9 +12,10 @@ using System.Timers;
namespace SafeExamBrowser.UserInterface.Desktop.ViewModels namespace SafeExamBrowser.UserInterface.Desktop.ViewModels
{ {
class DateTimeViewModel : INotifyPropertyChanged internal class DateTimeViewModel : INotifyPropertyChanged
{ {
private Timer timer; private Timer timer;
private readonly bool showSeconds;
public string Date { get; private set; } public string Date { get; private set; }
public string Time { get; private set; } public string Time { get; private set; }
@ -22,11 +23,12 @@ namespace SafeExamBrowser.UserInterface.Desktop.ViewModels
public event PropertyChangedEventHandler PropertyChanged; public event PropertyChangedEventHandler PropertyChanged;
public DateTimeViewModel() public DateTimeViewModel(bool showSeconds)
{ {
timer = new Timer(1000); this.showSeconds = showSeconds;
timer.Elapsed += Timer_Elapsed; this.timer = new Timer(1000);
timer.Start(); this.timer.Elapsed += Timer_Elapsed;
this.timer.Start();
} }
private void Timer_Elapsed(object sender, ElapsedEventArgs e) private void Timer_Elapsed(object sender, ElapsedEventArgs e)
@ -34,7 +36,7 @@ namespace SafeExamBrowser.UserInterface.Desktop.ViewModels
var date = DateTime.Now; var date = DateTime.Now;
Date = date.ToShortDateString(); Date = date.ToShortDateString();
Time = date.ToShortTimeString(); Time = showSeconds ? date.ToLongTimeString() : date.ToShortTimeString();
ToolTip = $"{date.ToLongDateString()} {date.ToLongTimeString()}"; ToolTip = $"{date.ToLongDateString()} {date.ToLongTimeString()}";
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Time))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Time)));