SEBWIN-308: Implemented basic file system dialog and download configuration for browser.

This commit is contained in:
dbuechel 2020-01-20 16:13:08 +01:00
parent 61f369a9a3
commit 0083a87bce
26 changed files with 881 additions and 14 deletions

View file

@ -106,6 +106,7 @@ namespace SafeExamBrowser.Browser
private void InitializeControl()
{
var contextMenuHandler = new ContextMenuHandler();
var dialogHandler = new DialogHandler();
var displayHandler = new DisplayHandler();
var downloadLogger = logger.CloneFor($"{nameof(DownloadHandler)} #{Id}");
var downloadHandler = new DownloadHandler(appConfig, settings, downloadLogger);
@ -117,6 +118,7 @@ namespace SafeExamBrowser.Browser
Icon = new BrowserIconResource();
dialogHandler.DialogRequested += DialogHandler_DialogRequested;
displayHandler.FaviconChanged += DisplayHandler_FaviconChanged;
displayHandler.ProgressChanged += DisplayHandler_ProgressChanged;
downloadHandler.ConfigurationDownloadRequested += DownloadHandler_ConfigurationDownloadRequested;
@ -130,7 +132,7 @@ namespace SafeExamBrowser.Browser
InitializeRequestFilter(requestFilter);
control = new BrowserControl(contextMenuHandler, displayHandler, downloadHandler, keyboardHandler, lifeSpanHandler, requestHandler, startUrl);
control = new BrowserControl(contextMenuHandler, dialogHandler, displayHandler, downloadHandler, keyboardHandler, lifeSpanHandler, requestHandler, startUrl);
control.AddressChanged += Control_AddressChanged;
control.LoadingStateChanged += Control_LoadingStateChanged;
control.TitleChanged += Control_TitleChanged;
@ -207,6 +209,23 @@ namespace SafeExamBrowser.Browser
TitleChanged?.Invoke(Title);
}
private void DialogHandler_DialogRequested(DialogRequestedEventArgs args)
{
var dialog = uiFactory.CreateFileSystemDialog(args.Element, args.InitialPath, args.Operation, title: args.Title);
var result = dialog.Show(window);
if (result.Success)
{
args.FullPath = result.FullPath;
args.Success = result.Success;
logger.Debug($"User selected path '{result.FullPath}' when asked to {args.Operation}->{args.Element}.");
}
else
{
logger.Debug($"User aborted file system dialog to {args.Operation}->{args.Element}.");
}
}
private void DisplayHandler_FaviconChanged(string uri)
{
var request = new HttpRequestMessage(HttpMethod.Head, uri);

View file

@ -16,6 +16,7 @@ namespace SafeExamBrowser.Browser
internal class BrowserControl : ChromiumWebBrowser, IBrowserControl
{
private IContextMenuHandler contextMenuHandler;
private IDialogHandler dialogHandler;
private IDisplayHandler displayHandler;
private IDownloadHandler downloadHandler;
private IKeyboardHandler keyboardHandler;
@ -49,6 +50,7 @@ namespace SafeExamBrowser.Browser
public BrowserControl(
IContextMenuHandler contextMenuHandler,
IDialogHandler dialogHandler,
IDisplayHandler displayHandler,
IDownloadHandler downloadHandler,
IKeyboardHandler keyboardHandler,
@ -57,6 +59,7 @@ namespace SafeExamBrowser.Browser
string url) : base(url)
{
this.contextMenuHandler = contextMenuHandler;
this.dialogHandler = dialogHandler;
this.displayHandler = displayHandler;
this.downloadHandler = downloadHandler;
this.keyboardHandler = keyboardHandler;
@ -70,6 +73,7 @@ namespace SafeExamBrowser.Browser
LoadingStateChanged += (o, args) => loadingStateChanged?.Invoke(args.IsLoading);
TitleChanged += (o, args) => titleChanged?.Invoke(args.Title);
DialogHandler = dialogHandler;
DisplayHandler = displayHandler;
DownloadHandler = downloadHandler;
KeyboardHandler = keyboardHandler;

View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2020 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 SafeExamBrowser.UserInterface.Contracts.FileSystemDialog;
namespace SafeExamBrowser.Browser.Events
{
internal class DialogRequestedEventArgs
{
internal FileSystemElement Element { get; set; }
internal string InitialPath { get; set; }
internal FileSystemOperation Operation { get; set; }
internal string FullPath { get; set; }
internal bool Success { get; set; }
internal string Title { get; set; }
}
}

View file

@ -0,0 +1,12 @@
/*
* Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
namespace SafeExamBrowser.Browser.Events
{
internal delegate void DialogRequestedEventHandler(DialogRequestedEventArgs args);
}

View file

@ -0,0 +1,73 @@
/*
* Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Collections.Generic;
using System.Threading.Tasks;
using CefSharp;
using SafeExamBrowser.Browser.Events;
using SafeExamBrowser.UserInterface.Contracts.FileSystemDialog;
namespace SafeExamBrowser.Browser.Handlers
{
internal class DialogHandler : IDialogHandler
{
internal event DialogRequestedEventHandler DialogRequested;
public bool OnFileDialog(IWebBrowser webBrowser, IBrowser browser, CefFileDialogMode mode, CefFileDialogFlags flags, string title, string defaultFilePath, List<string> acceptFilters, int selectedAcceptFilter, IFileDialogCallback callback)
{
var args = new DialogRequestedEventArgs
{
Element = ToElement(mode),
InitialPath = defaultFilePath,
Operation = ToOperation(mode),
Title = title
};
Task.Run(() =>
{
DialogRequested?.Invoke(args);
using (callback)
{
if (args.Success)
{
callback.Continue(selectedAcceptFilter, new List<string> { args.FullPath });
}
else
{
callback.Cancel();
}
}
});
return true;
}
private FileSystemElement ToElement(CefFileDialogMode mode)
{
switch (mode)
{
case CefFileDialogMode.OpenFolder:
return FileSystemElement.Folder;
default:
return FileSystemElement.File;
}
}
private FileSystemOperation ToOperation(CefFileDialogMode mode)
{
switch (mode)
{
case CefFileDialogMode.Save:
return FileSystemOperation.Save;
default:
return FileSystemOperation.Open;
}
}
}
}

View file

@ -14,6 +14,7 @@ using CefSharp;
using SafeExamBrowser.Browser.Contracts.Events;
using SafeExamBrowser.Configuration.Contracts;
using SafeExamBrowser.Logging.Contracts;
using Syroot.Windows.IO;
using BrowserSettings = SafeExamBrowser.Settings.Browser.BrowserSettings;
namespace SafeExamBrowser.Browser.Handlers
@ -25,9 +26,9 @@ namespace SafeExamBrowser.Browser.Handlers
private ConcurrentDictionary<int, DownloadFinishedCallback> callbacks;
private ILogger logger;
public event DownloadRequestedEventHandler ConfigurationDownloadRequested;
internal event DownloadRequestedEventHandler ConfigurationDownloadRequested;
public DownloadHandler(AppConfig appConfig, BrowserSettings settings, ILogger logger)
internal DownloadHandler(AppConfig appConfig, BrowserSettings settings, ILogger logger)
{
this.appConfig = appConfig;
this.callbacks = new ConcurrentDictionary<int, DownloadFinishedCallback>();
@ -41,7 +42,7 @@ namespace SafeExamBrowser.Browser.Handlers
var extension = Path.GetExtension(uri.AbsolutePath);
var isConfigFile = String.Equals(extension, appConfig.ConfigurationFileExtension, StringComparison.OrdinalIgnoreCase);
logger.Debug($"Handling download request for '{uri}'.");
logger.Debug($"Detected download request for '{uri}'.");
if (isConfigFile)
{
@ -49,12 +50,7 @@ namespace SafeExamBrowser.Browser.Handlers
}
else if (settings.AllowDownloads)
{
logger.Debug($"Starting download of '{uri}'...");
using (callback)
{
callback.Continue(null, true);
}
Task.Run(() => HandleFileDownload(downloadItem, callback));
}
else
{
@ -64,6 +60,8 @@ namespace SafeExamBrowser.Browser.Handlers
public void OnDownloadUpdated(IWebBrowser webBrowser, IBrowser browser, DownloadItem downloadItem, IDownloadItemCallback callback)
{
// TODO: Show download progress in respective window -> event for BrowserApplicationInstance!
if (downloadItem.IsComplete || downloadItem.IsCancelled)
{
if (callbacks.TryRemove(downloadItem.Id, out DownloadFinishedCallback finished) && finished != null)
@ -72,6 +70,39 @@ namespace SafeExamBrowser.Browser.Handlers
}
logger.Debug($"Download of '{downloadItem.Url}' {(downloadItem.IsComplete ? "is complete" : "was cancelled")}.");
// TODO: Show success message or download icon like Firefox in respective window!
}
}
private void HandleFileDownload(DownloadItem downloadItem, IBeforeDownloadCallback callback)
{
var filePath = default(string);
var showDialog = settings.AllowCustomDownloadLocation;
logger.Debug($"Handling download of file '{downloadItem.SuggestedFileName}'.");
if (!string.IsNullOrEmpty(settings.DownloadDirectory))
{
filePath = Path.Combine(Environment.ExpandEnvironmentVariables(settings.DownloadDirectory), downloadItem.SuggestedFileName);
}
else
{
filePath = Path.Combine(KnownFolders.Downloads.ExpandedPath, downloadItem.SuggestedFileName);
}
if (showDialog)
{
logger.Debug($"Allowing user to select custom download location, with '{filePath}' as suggestion.");
}
else
{
logger.Debug($"Automatically downloading file as '{filePath}'.");
}
using (callback)
{
callback.Continue(filePath, showDialog);
}
}
@ -79,7 +110,7 @@ namespace SafeExamBrowser.Browser.Handlers
{
var args = new DownloadEventArgs();
logger.Debug($"Detected download request for configuration file '{downloadItem.Url}'.");
logger.Debug($"Handling download of configuration file '{downloadItem.SuggestedFileName}'.");
ConfigurationDownloadRequested?.Invoke(downloadItem.SuggestedFileName, args);
if (args.AllowDownload)
@ -89,7 +120,7 @@ namespace SafeExamBrowser.Browser.Handlers
callbacks[downloadItem.Id] = args.Callback;
}
logger.Debug($"Starting download of configuration file '{downloadItem.Url}'...");
logger.Debug($"Starting download of configuration file '{downloadItem.SuggestedFileName}'...");
using (callback)
{
@ -98,7 +129,7 @@ namespace SafeExamBrowser.Browser.Handlers
}
else
{
logger.Debug($"Download of configuration file '{downloadItem.Url}' was cancelled.");
logger.Debug($"Download of configuration file '{downloadItem.SuggestedFileName}' was cancelled.");
}
}
}

View file

@ -54,6 +54,9 @@
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<ItemGroup>
<Reference Include="Syroot.KnownFolders, Version=1.2.1.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Syroot.Windows.IO.KnownFolders.1.2.1\lib\net452\Syroot.KnownFolders.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Drawing" />
<Reference Include="System.Net.Http" />
@ -63,6 +66,8 @@
<ItemGroup>
<Compile Include="BrowserApplication.cs" />
<Compile Include="BrowserApplicationInstance.cs" />
<Compile Include="Events\DialogRequestedEventArgs.cs" />
<Compile Include="Events\DialogRequestedEventHandler.cs" />
<Compile Include="Events\FaviconChangedEventHandler.cs" />
<Compile Include="Events\InstanceTerminatedEventHandler.cs" />
<Compile Include="Events\PopupRequestedEventArgs.cs" />
@ -78,6 +83,7 @@
<SubType>Component</SubType>
</Compile>
<Compile Include="BrowserIconResource.cs" />
<Compile Include="Handlers\DialogHandler.cs" />
<Compile Include="Handlers\DisplayHandler.cs" />
<Compile Include="Handlers\DownloadHandler.cs" />
<Compile Include="Handlers\KeyboardHandler.cs" />

View file

@ -4,4 +4,5 @@
<package id="cef.redist.x86" version="75.1.14" targetFramework="net472" />
<package id="CefSharp.Common" version="75.1.142" targetFramework="net472" />
<package id="CefSharp.WinForms" version="75.1.142" targetFramework="net472" />
<package id="Syroot.Windows.IO.KnownFolders" version="1.2.1" targetFramework="net472" />
</packages>

View file

@ -24,6 +24,9 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping
case Keys.Browser.AllowConfigurationDownloads:
MapAllowConfigurationDownloads(settings, value);
break;
case Keys.Browser.AllowCustomDownloadLocation:
MapAllowCustomDownloadLocation(settings, value);
break;
case Keys.Browser.AllowDeveloperConsole:
MapAllowDeveloperConsole(settings, value);
break;
@ -54,6 +57,9 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping
case Keys.Browser.AdditionalWindow.WindowWidth:
MapWindowWidthAdditionalWindow(settings, value);
break;
case Keys.Browser.DownloadDirectory:
MapDownloadDirectory(settings, value);
break;
case Keys.Browser.EnableBrowser:
MapEnableBrowser(settings, value);
break;
@ -139,6 +145,14 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping
}
}
private void MapAllowCustomDownloadLocation(AppSettings settings, object value)
{
if (value is bool allow)
{
settings.Browser.AllowCustomDownloadLocation = allow;
}
}
private void MapAllowDeveloperConsole(AppSettings settings, object value)
{
if (value is bool allow)
@ -198,6 +212,14 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping
}
}
private void MapDownloadDirectory(AppSettings settings, object value)
{
if (value is string directory)
{
settings.Browser.DownloadDirectory = directory;
}
}
private void MapEnableBrowser(AppSettings settings, object value)
{
if (value is bool enable)

View file

@ -42,11 +42,13 @@ namespace SafeExamBrowser.Configuration.ConfigurationData
internal static class Browser
{
internal const string AllowConfigurationDownloads = "downloadAndOpenSebConfig";
internal const string AllowCustomDownloadLocation = "allowCustomDownloadLocation";
internal const string AllowDeveloperConsole = "allowDeveloperConsole";
internal const string AllowDownloads = "allowDownUploads";
internal const string AllowPageZoom = "enableZoomPage";
internal const string CustomUserAgentDesktop = "browserUserAgentWinDesktopModeCustom";
internal const string CustomUserAgentMobile = "browserUserAgentWinTouchModeCustom";
internal const string DownloadDirectory = "downloadDirectoryWin";
internal const string EnableBrowser = "enableSebBrowser";
internal const string PopupPolicy = "newBrowserWindowByLinkPolicy";
internal const string PopupBlockForeignHost = "newBrowserWindowByLinkBlockForeign";

View file

@ -23,6 +23,18 @@ namespace SafeExamBrowser.I18n.Contracts
BrowserWindow_DeveloperConsoleMenuItem,
BrowserWindow_ZoomMenuItem,
Build,
FileSystemDialog_Cancel,
FileSystemDialog_LoadError,
FileSystemDialog_Loading,
FileSystemDialog_OpenFileMessage,
FileSystemDialog_OpenFolderMessage,
FileSystemDialog_OverwriteWarning,
FileSystemDialog_OverwriteWarningTitle,
FileSystemDialog_SaveAs,
FileSystemDialog_SaveFileMessage,
FileSystemDialog_SaveFolderMessage,
FileSystemDialog_Select,
FileSystemDialog_Title,
FolderDialog_ApplicationLocation,
LockScreen_AllowOption,
LockScreen_Message,

View file

@ -27,6 +27,42 @@
<Entry key="Build">
Build
</Entry>
<Entry key="FileSystemDialog_Cancel">
Cancel
</Entry>
<Entry key="FileSystemDialog_LoadError">
Failed to load data!
</Entry>
<Entry key="FileSystemDialog_Loading">
Loading...
</Entry>
<Entry key="FileSystemDialog_OpenFileMessage">
Please select a file to open.
</Entry>
<Entry key="FileSystemDialog_OpenFolderMessage">
Please select a folder.
</Entry>
<Entry key="FileSystemDialog_OverwriteWarning">
The selected file already exists! Would you really like to overwrite it?
</Entry>
<Entry key="FileSystemDialog_OverwriteWarningTitle">
Overwrite?
</Entry>
<Entry key="FileSystemDialog_SaveAs">
Save as:
</Entry>
<Entry key="FileSystemDialog_SaveFileMessage">
Please select a location to save the file.
</Entry>
<Entry key="FileSystemDialog_SaveFolderMessage">
Please select a location to save the folder.
</Entry>
<Entry key="FileSystemDialog_Select">
Select
</Entry>
<Entry key="FileSystemDialog_Title">
File System Access
</Entry>
<Entry key="FolderDialog_ApplicationLocation">
Application "%%NAME%%" could not be found on the system! Please locate the folder containing the main executable "%%EXECUTABLE%%".
</Entry>

View file

@ -26,6 +26,11 @@ namespace SafeExamBrowser.Settings.Browser
/// </summary>
public bool AllowConfigurationDownloads { get; set; }
/// <summary>
/// Determines whether the user will be allowed to select a custom location when downloading a file (excluding configuration files).
/// </summary>
public bool AllowCustomDownloadLocation { get; set; }
/// <summary>
/// Determines whether the user will be allowed to download files (excluding configuration files).
/// </summary>
@ -46,6 +51,11 @@ namespace SafeExamBrowser.Settings.Browser
/// </summary>
public string CustomUserAgent { get; set; }
/// <summary>
/// Defines a custom directory for file downloads. If not defined, all downloads will be saved in the current user's download directory.
/// </summary>
public string DownloadDirectory { get; set; }
/// <summary>
/// Determines whether the user is allowed to use the integrated browser application.
/// </summary>

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
namespace SafeExamBrowser.UserInterface.Contracts.FileSystemDialog
{
/// <summary>
/// Defines the user interaction result of an <see cref="IFileSystemDialog"/>.
/// </summary>
public class FileSystemDialogResult
{
/// <summary>
/// The full path of the item selected by the user, or <c>null</c> if the interaction was unsuccessful.
/// </summary>
public string FullPath { get; set; }
/// <summary>
/// Indicates whether the user confirmed the dialog or not.
/// </summary>
public bool Success { get; set; }
}
}

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
namespace SafeExamBrowser.UserInterface.Contracts.FileSystemDialog
{
/// <summary>
/// Defines all elements supported by an <see cref="IFileSystemDialog"/>
/// </summary>
public enum FileSystemElement
{
/// <summary>
/// A dialog to perform an operation with a file.
/// </summary>
File,
/// <summary>
/// A dialog to perform an operation with a folder.
/// </summary>
Folder
}
}

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
namespace SafeExamBrowser.UserInterface.Contracts.FileSystemDialog
{
/// <summary>
/// Defines all operations supported by an <see cref="IFileSystemDialog"/>
/// </summary>
public enum FileSystemOperation
{
/// <summary>
/// A dialog to open a file system element.
/// </summary>
Open,
/// <summary>
/// A dialog to save a file system element.
/// </summary>
Save
}
}

View file

@ -0,0 +1,20 @@
/*
* Copyright (c) 2020 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 SafeExamBrowser.UserInterface.Contracts.Windows;
namespace SafeExamBrowser.UserInterface.Contracts.FileSystemDialog
{
public interface IFileSystemDialog
{
/// <summary>
/// Shows the dialog to the user. If a parent window is specified, the dialog is rendered modally for the given parent.
/// </summary>
FileSystemDialogResult Show(IWindow parent = null);
}
}

View file

@ -18,6 +18,7 @@ using SafeExamBrowser.SystemComponents.Contracts.Keyboard;
using SafeExamBrowser.SystemComponents.Contracts.PowerSupply;
using SafeExamBrowser.SystemComponents.Contracts.WirelessNetwork;
using SafeExamBrowser.UserInterface.Contracts.Browser;
using SafeExamBrowser.UserInterface.Contracts.FileSystemDialog;
using SafeExamBrowser.UserInterface.Contracts.Shell;
using SafeExamBrowser.UserInterface.Contracts.Windows;
using SafeExamBrowser.UserInterface.Contracts.Windows.Data;
@ -49,6 +50,11 @@ namespace SafeExamBrowser.UserInterface.Contracts
/// </summary>
IBrowserWindow CreateBrowserWindow(IBrowserControl control, BrowserSettings settings, bool isMainWindow);
/// <summary>
/// Creates a file system dialog according to the given parameters.
/// </summary>
IFileSystemDialog CreateFileSystemDialog(FileSystemElement element, string initialPath, FileSystemOperation operation, string message = default(string), string title = default(string));
/// <summary>
/// Creates a folder dialog with the given message.
/// </summary>

View file

@ -60,6 +60,10 @@
<Compile Include="Browser\IBrowserControl.cs" />
<Compile Include="Browser\IBrowserWindow.cs" />
<Compile Include="Events\ActionRequestedEventHandler.cs" />
<Compile Include="FileSystemDialog\FileSystemDialogResult.cs" />
<Compile Include="FileSystemDialog\FileSystemElement.cs" />
<Compile Include="FileSystemDialog\FileSystemOperation.cs" />
<Compile Include="FileSystemDialog\IFileSystemDialog.cs" />
<Compile Include="IProgressIndicator.cs" />
<Compile Include="IUserInterfaceFactory.cs" />
<Compile Include="MessageBox\IMessageBox.cs" />

View file

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

View file

@ -0,0 +1,382 @@
/*
* Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using FontAwesome.WPF;
using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.UserInterface.Contracts.FileSystemDialog;
using SafeExamBrowser.UserInterface.Contracts.Windows;
using SafeExamBrowser.UserInterface.Shared.Utilities;
namespace SafeExamBrowser.UserInterface.Desktop
{
public partial class FileSystemDialog : Window, IFileSystemDialog
{
private FileSystemElement element;
private string initialPath;
private string message;
private FileSystemOperation operation;
private IText text;
private string title;
public FileSystemDialog(
FileSystemElement element,
string initialPath,
FileSystemOperation operation,
IText text,
string message = default(string),
string title = default(string))
{
this.element = element;
this.initialPath = initialPath;
this.message = message;
this.operation = operation;
this.text = text;
this.title = title;
InitializeComponent();
InitializeDialog();
}
public FileSystemDialogResult Show(IWindow parent = null)
{
return Dispatcher.Invoke(() =>
{
var result = new FileSystemDialogResult();
if (parent is Window)
{
Owner = parent as Window;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
}
if (ShowDialog() == true)
{
result.FullPath = BuildFullPath();
result.Success = true;
}
return result;
});
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
private void FileSystem_Expanded(object sender, RoutedEventArgs e)
{
if (e.Source is TreeViewItem item && item.Items.Count == 1 && !(item.Items[0] is TreeViewItem))
{
Load(item);
}
}
private void FileSystem_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
if (e.NewValue is TreeViewItem item)
{
if (item.Tag is DirectoryInfo directory)
{
SelectButton.IsEnabled = element == FileSystemElement.Folder || operation == FileSystemOperation.Save;
SelectedElement.Text = directory.FullName;
SelectedElement.ToolTip = directory.FullName;
}
else if (item.Tag is FileInfo file)
{
SelectButton.IsEnabled = element == FileSystemElement.File;
SelectedElement.Text = file.FullName;
SelectedElement.ToolTip = file.FullName;
}
else
{
SelectButton.IsEnabled = false;
}
}
}
private void NewElementName_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter && IsValid())
{
DialogResult = true;
Close();
}
}
private void SelectButton_Click(object sender, RoutedEventArgs e)
{
if (IsValid())
{
DialogResult = true;
Close();
}
}
private string BuildFullPath()
{
var fullPath = SelectedElement.Text;
if (operation == FileSystemOperation.Save)
{
fullPath = Path.Combine(SelectedElement.Text, NewElementName.Text);
if (element == FileSystemElement.File)
{
var extension = Path.GetExtension(initialPath);
if (!fullPath.EndsWith(extension))
{
fullPath = $"{fullPath}{extension}";
}
}
}
return fullPath;
}
private bool IsValid()
{
var fullPath = BuildFullPath();
var isValid = true;
if (element == FileSystemElement.File && operation == FileSystemOperation.Save && File.Exists(fullPath))
{
var message = text.Get(TextKey.FileSystemDialog_OverwriteWarning);
var title = text.Get(TextKey.FileSystemDialog_OverwriteWarningTitle);
var result = System.Windows.MessageBox.Show(this, message, title, MessageBoxButton.YesNo, MessageBoxImage.Warning);
isValid = result == MessageBoxResult.Yes;
}
return isValid;
}
private void Load(TreeViewItem item)
{
item.Items.Clear();
if (item.Tag is DirectoryInfo directory)
{
FileSystem.Cursor = Cursors.Wait;
item.BeginInit();
try
{
foreach (var subDirectory in directory.GetDirectories())
{
if (!subDirectory.Attributes.HasFlag(FileAttributes.Hidden))
{
item.Items.Add(CreateItem(subDirectory));
}
}
foreach (var file in directory.GetFiles())
{
if (!file.Attributes.HasFlag(FileAttributes.Hidden))
{
item.Items.Add(CreateItem(file));
}
}
}
catch (Exception e)
{
item.Items.Add(CreateErrorItem(e));
}
item.EndInit();
FileSystem.Cursor = Cursors.Arrow;
}
}
private TreeViewItem CreateErrorItem(Exception e)
{
var item = new TreeViewItem();
item.Foreground = Brushes.Red;
item.Header = $"{text.Get(TextKey.FileSystemDialog_LoadError)} {e.Message}";
item.ToolTip = e.GetType() + Environment.NewLine + e.StackTrace;
return item;
}
private TreeViewItem CreateItem(DirectoryInfo directory)
{
var header = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(2) };
var image = new Image
{
Height = 16,
Source = IconLoader.LoadIconFor(directory)
};
var item = new TreeViewItem();
var textBlock = new TextBlock { Margin = new Thickness(5, 0, 0, 0), Text = directory.Name, VerticalAlignment = VerticalAlignment.Center };
header.Children.Add(image);
header.Children.Add(textBlock);
item.Cursor = Cursors.Hand;
item.Header = header;
item.Tag = directory;
item.ToolTip = directory.FullName;
item.Items.Add(text.Get(TextKey.FileSystemDialog_Loading));
return item;
}
private TreeViewItem CreateItem(FileInfo file)
{
var header = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(2) };
var image = new Image
{
Height = 20,
Source = IconLoader.LoadIconFor(file)
};
var item = new TreeViewItem();
var textBlock = new TextBlock { Margin = new Thickness(5, 0, 0, 0), Text = file.Name, VerticalAlignment = VerticalAlignment.Center };
header.Children.Add(image);
header.Children.Add(textBlock);
item.Header = header;
item.Tag = file;
item.ToolTip = file.FullName;
if (element == FileSystemElement.File && operation == FileSystemOperation.Open)
{
item.Cursor = Cursors.Hand;
}
else
{
item.Cursor = Cursors.No;
item.Focusable = false;
textBlock.Foreground = Brushes.Gray;
}
return item;
}
private void InitializeDialog()
{
CancelButton.Click += CancelButton_Click;
CancelButton.Content = text.Get(TextKey.FileSystemDialog_Cancel);
FileSystem.SelectedItemChanged += FileSystem_SelectedItemChanged;
NewElement.Visibility = operation == FileSystemOperation.Save ? Visibility.Visible : Visibility.Collapsed;
NewElementLabel.Text = text.Get(TextKey.FileSystemDialog_SaveAs);
NewElementName.KeyUp += NewElementName_KeyUp;
OperationIcon.Icon = operation == FileSystemOperation.Save ? FontAwesomeIcon.Download : FontAwesomeIcon.Search;
SelectButton.Click += SelectButton_Click;
SelectButton.Content = text.Get(TextKey.FileSystemDialog_Select);
InitializeText();
InitializeFileSystem();
}
private void InitializeFileSystem()
{
foreach (var drive in DriveInfo.GetDrives())
{
FileSystem.Items.Add(CreateItem(drive.RootDirectory));
}
if (FileSystem.HasItems && FileSystem.Items[0] is TreeViewItem item)
{
item.IsSelected = true;
}
if (!string.IsNullOrEmpty(initialPath))
{
var pathRoot = Path.GetPathRoot(initialPath);
var directories = initialPath.Replace(pathRoot, "").Split(Path.DirectorySeparatorChar);
var segments = new List<string>();
segments.Add(pathRoot);
segments.AddRange(directories);
AutoSelect(FileSystem.Items, segments);
if (element == FileSystemElement.File && operation == FileSystemOperation.Save)
{
NewElementName.Text = Path.GetFileName(initialPath);
}
}
}
private void AutoSelect(ItemCollection items, List<string> segments)
{
var segment = segments.FirstOrDefault();
if (segment != default(string))
{
foreach (var item in items)
{
if (item is TreeViewItem i && i.Tag is DirectoryInfo d && d.Name.Equals(segment))
{
i.IsExpanded = true;
i.IsSelected = true;
i.BringIntoView();
AutoSelect(i.Items, segments.Skip(1).ToList());
break;
}
}
}
}
private void InitializeText()
{
if (string.IsNullOrEmpty(message))
{
if (element == FileSystemElement.File)
{
if (operation == FileSystemOperation.Open)
{
Message.Text = text.Get(TextKey.FileSystemDialog_OpenFileMessage);
}
else
{
Message.Text = text.Get(TextKey.FileSystemDialog_SaveFileMessage);
}
}
else
{
if (operation == FileSystemOperation.Open)
{
Message.Text = text.Get(TextKey.FileSystemDialog_OpenFolderMessage);
}
else
{
Message.Text = text.Get(TextKey.FileSystemDialog_SaveFolderMessage);
}
}
}
else
{
Message.Text = message;
}
if (string.IsNullOrEmpty(title))
{
Title = text.Get(TextKey.FileSystemDialog_Title);
}
else
{
Title = title;
}
}
}
}

View file

@ -145,6 +145,9 @@
<Compile Include="Controls\TaskviewWindowControl.xaml.cs">
<DependentUpon>TaskviewWindowControl.xaml</DependentUpon>
</Compile>
<Compile Include="FileSystemDialog.xaml.cs">
<DependentUpon>FileSystemDialog.xaml</DependentUpon>
</Compile>
<Compile Include="FolderDialog.cs" />
<Compile Include="LockScreen.xaml.cs">
<DependentUpon>LockScreen.xaml</DependentUpon>
@ -322,6 +325,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="FileSystemDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="LockScreen.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>

View file

@ -23,6 +23,7 @@ using SafeExamBrowser.SystemComponents.Contracts.PowerSupply;
using SafeExamBrowser.SystemComponents.Contracts.WirelessNetwork;
using SafeExamBrowser.UserInterface.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Browser;
using SafeExamBrowser.UserInterface.Contracts.FileSystemDialog;
using SafeExamBrowser.UserInterface.Contracts.Shell;
using SafeExamBrowser.UserInterface.Contracts.Windows;
using SafeExamBrowser.UserInterface.Contracts.Windows.Data;
@ -72,7 +73,12 @@ namespace SafeExamBrowser.UserInterface.Desktop
public IBrowserWindow CreateBrowserWindow(IBrowserControl control, BrowserSettings settings, bool isMainWindow)
{
return new BrowserWindow(control, settings, isMainWindow, text);
return Application.Current.Dispatcher.Invoke(() => new BrowserWindow(control, settings, isMainWindow, text));
}
public IFileSystemDialog CreateFileSystemDialog(FileSystemElement element, string initialPath, FileSystemOperation operation, string message = default(string), string title = default(string))
{
return Application.Current.Dispatcher.Invoke(() => new FileSystemDialog(element, initialPath, operation, text, message, title));
}
public IFolderDialog CreateFolderDialog(string message)

View file

@ -23,6 +23,7 @@ using SafeExamBrowser.SystemComponents.Contracts.PowerSupply;
using SafeExamBrowser.SystemComponents.Contracts.WirelessNetwork;
using SafeExamBrowser.UserInterface.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Browser;
using SafeExamBrowser.UserInterface.Contracts.FileSystemDialog;
using SafeExamBrowser.UserInterface.Contracts.Shell;
using SafeExamBrowser.UserInterface.Contracts.Windows;
using SafeExamBrowser.UserInterface.Contracts.Windows.Data;
@ -75,6 +76,11 @@ namespace SafeExamBrowser.UserInterface.Mobile
return new BrowserWindow(control, settings, isMainWindow, text);
}
public IFileSystemDialog CreateFileSystemDialog(FileSystemElement element, string initialPath, FileSystemOperation operation, string message = default(string), string title = default(string))
{
throw new System.NotImplementedException();
}
public IFolderDialog CreateFolderDialog(string message)
{
return new FolderDialog(message);

View file

@ -72,6 +72,7 @@
<Compile Include="Properties\AssemblyInfo.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Utilities\IconLoader.cs" />
<Compile Include="Utilities\IconResourceLoader.cs" />
<Compile Include="Utilities\Thumbnail.cs" />
<Compile Include="Utilities\VisualExtensions.cs" />

View file

@ -0,0 +1,62 @@
/*
* Copyright (c) 2020 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.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace SafeExamBrowser.UserInterface.Shared.Utilities
{
public static class IconLoader
{
private const uint SHGFI_ICON = 0x100;
private const uint SHGFI_LARGEICON = 0x0;
private const uint SHGFI_SMALLICON = 0x1;
public static ImageSource LoadIconFor(DirectoryInfo directory)
{
var fileInfo = new SHFILEINFO();
var result = SHGetFileInfo(directory.FullName, 0, ref fileInfo, (uint) Marshal.SizeOf(fileInfo), SHGFI_ICON | SHGFI_SMALLICON);
var imageSource = Imaging.CreateBitmapSourceFromHIcon(fileInfo.hIcon, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
DestroyIcon(fileInfo.hIcon);
return imageSource;
}
public static ImageSource LoadIconFor(FileInfo file)
{
using (var icon = System.Drawing.Icon.ExtractAssociatedIcon(file.FullName))
{
return Imaging.CreateBitmapSourceFromHIcon(icon.Handle, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
}
}
[DllImport("user32.dll")]
private static extern int DestroyIcon(IntPtr hIcon);
[DllImport("shell32.dll")]
private static extern IntPtr SHGetFileInfo(string pszPath, uint dwFileAttributes, ref SHFILEINFO psfi, uint cbSizeFileInfo, uint uFlags);
[StructLayout(LayoutKind.Sequential)]
private struct SHFILEINFO
{
public IntPtr hIcon;
public IntPtr iIcon;
public uint dwAttributes;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string szDisplayName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
public string szTypeName;
};
}
}