diff --git a/lib/TweetLib.Browser.CEF/Dialogs/FileDialogType.cs b/lib/TweetLib.Browser.CEF/Dialogs/FileDialogType.cs new file mode 100644 index 00000000..58297465 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Dialogs/FileDialogType.cs @@ -0,0 +1,7 @@ +namespace TweetLib.Browser.CEF.Dialogs { + public enum FileDialogType { + Open, + OpenMultiple, + Other + } +} diff --git a/lib/TweetLib.Browser.CEF/Dialogs/JsDialogType.cs b/lib/TweetLib.Browser.CEF/Dialogs/JsDialogType.cs new file mode 100644 index 00000000..f6d83560 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Dialogs/JsDialogType.cs @@ -0,0 +1,8 @@ +namespace TweetLib.Browser.CEF.Dialogs { + public enum JsDialogType { + Alert, + Confirm, + Prompt, + Unknown + } +} diff --git a/lib/TweetLib.Browser.CEF/Dialogs/MessageDialogType.cs b/lib/TweetLib.Browser.CEF/Dialogs/MessageDialogType.cs new file mode 100644 index 00000000..9bf5a00d --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Dialogs/MessageDialogType.cs @@ -0,0 +1,9 @@ +namespace TweetLib.Browser.CEF.Dialogs { + public enum MessageDialogType { + None, + Question, + Information, + Warning, + Error + } +} diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IFileDialogCallbackAdapter.cs b/lib/TweetLib.Browser.CEF/Interfaces/IFileDialogCallbackAdapter.cs new file mode 100644 index 00000000..c1759ee7 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Interfaces/IFileDialogCallbackAdapter.cs @@ -0,0 +1,7 @@ +namespace TweetLib.Browser.CEF.Interfaces { + public interface IFileDialogCallbackAdapter<T> { + void Continue(T callback, int selectedAcceptFilter, string[] filePaths); + void Cancel(T callback); + void Dispose(T callback); + } +} diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IFileDialogOpener.cs b/lib/TweetLib.Browser.CEF/Interfaces/IFileDialogOpener.cs new file mode 100644 index 00000000..fb449091 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Interfaces/IFileDialogOpener.cs @@ -0,0 +1,8 @@ +using System; +using System.Collections.Generic; + +namespace TweetLib.Browser.CEF.Interfaces { + public interface IFileDialogOpener { + void OpenFile(string title, bool multiple, List<string> supportedExtensions, Action<string[]> onAccepted, Action onCancelled); + } +} diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IJsDialogCallbackAdapter.cs b/lib/TweetLib.Browser.CEF/Interfaces/IJsDialogCallbackAdapter.cs new file mode 100644 index 00000000..3fbc0180 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Interfaces/IJsDialogCallbackAdapter.cs @@ -0,0 +1,6 @@ +namespace TweetLib.Browser.CEF.Interfaces { + public interface IJsDialogCallbackAdapter<T> { + void Continue(T callback, bool success, string? userInput = null); + void Dispose(T callback); + } +} diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IJsDialogOpener.cs b/lib/TweetLib.Browser.CEF/Interfaces/IJsDialogOpener.cs new file mode 100644 index 00000000..2522cb72 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Interfaces/IJsDialogOpener.cs @@ -0,0 +1,10 @@ +using System; +using TweetLib.Browser.CEF.Dialogs; + +namespace TweetLib.Browser.CEF.Interfaces { + public interface IJsDialogOpener { + void Alert(MessageDialogType type, string title, string message, Action<bool> callback); + void Confirm(MessageDialogType type, string title, string message, Action<bool> callback); + void Prompt(MessageDialogType type, string title, string message, Action<bool, string> callback); + } +} diff --git a/lib/TweetLib.Browser.CEF/Logic/DialogHandlerLogic.cs b/lib/TweetLib.Browser.CEF/Logic/DialogHandlerLogic.cs new file mode 100644 index 00000000..920bcd3e --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Logic/DialogHandlerLogic.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using TweetLib.Browser.CEF.Dialogs; +using TweetLib.Browser.CEF.Interfaces; +using TweetLib.Utils.Static; + +namespace TweetLib.Browser.CEF.Logic { + public sealed class DialogHandlerLogic<TCallback> { + private readonly IFileDialogOpener fileDialogOpener; + private readonly IFileDialogCallbackAdapter<TCallback> callbackAdapter; + + public DialogHandlerLogic(IFileDialogOpener fileDialogOpener, IFileDialogCallbackAdapter<TCallback> callbackAdapter) { + this.fileDialogOpener = fileDialogOpener; + this.callbackAdapter = callbackAdapter; + } + + public bool OnFileDialog(FileDialogType type, IEnumerable<string> acceptFilters, TCallback callback) { + if (type is FileDialogType.Open or FileDialogType.OpenMultiple) { + var multiple = type == FileDialogType.OpenMultiple; + var supportedExtensions = acceptFilters.SelectMany(ParseFileType).Where(static filter => !string.IsNullOrEmpty(filter)).ToList(); + + fileDialogOpener.OpenFile("Open Files", multiple, supportedExtensions, files => { + string ext = Path.GetExtension(files[0])!.ToLower(); + callbackAdapter.Continue(callback, supportedExtensions.FindIndex(filter => ParseFileType(filter).Contains(ext)), files); + callbackAdapter.Dispose(callback); + }, () => { + callbackAdapter.Cancel(callback); + callbackAdapter.Dispose(callback); + }); + + return true; + } + else { + callbackAdapter.Dispose(callback); + return false; + } + } + + private static IEnumerable<string> ParseFileType(string type) { + if (string.IsNullOrEmpty(type)) { + return StringUtils.EmptyArray; + } + + if (type[0] == '.') { + return new string[] { type }; + } + + string[] extensions = type switch { + "image/jpeg" => new string[] { ".jpg", ".jpeg" }, + "image/png" => new string[] { ".png" }, + "image/gif" => new string[] { ".gif" }, + "image/webp" => new string[] { ".webp" }, + "video/mp4" => new string[] { ".mp4" }, + "video/quicktime" => new string[] { ".mov", ".qt" }, + _ => StringUtils.EmptyArray + }; + + if (extensions.Length == 0) { + Debug.WriteLine("Unknown file type: " + type); + Debugger.Break(); + } + + return extensions; + } + } +} diff --git a/lib/TweetLib.Browser.CEF/Logic/JsDialogHandlerLogic.cs b/lib/TweetLib.Browser.CEF/Logic/JsDialogHandlerLogic.cs new file mode 100644 index 00000000..3309fffc --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Logic/JsDialogHandlerLogic.cs @@ -0,0 +1,71 @@ +using System; +using TweetLib.Browser.CEF.Dialogs; +using TweetLib.Browser.CEF.Interfaces; + +namespace TweetLib.Browser.CEF.Logic { + public sealed class JsDialogHandlerLogic<TCallback> { + private static (MessageDialogType, string) GetMessageDialogProperties(string text) { + MessageDialogType type = MessageDialogType.None; + + int pipe = text.IndexOf('|'); + if (pipe != -1) { + type = text.Substring(0, pipe) switch { + "error" => MessageDialogType.Error, + "warning" => MessageDialogType.Warning, + "info" => MessageDialogType.Information, + "question" => MessageDialogType.Question, + _ => MessageDialogType.None + }; + + if (type != MessageDialogType.None) { + text = text.Substring(pipe + 1); + } + } + + return (type, text); + } + + private readonly IJsDialogOpener jsDialogOpener; + private readonly IJsDialogCallbackAdapter<TCallback> callbackAdapter; + + public JsDialogHandlerLogic(IJsDialogOpener jsDialogOpener, IJsDialogCallbackAdapter<TCallback> callbackAdapter) { + this.jsDialogOpener = jsDialogOpener; + this.callbackAdapter = callbackAdapter; + } + + public bool OnJSDialog(JsDialogType dialogType, string messageText, TCallback callback, out bool suppressMessage) { + suppressMessage = false; + + var (type, text) = GetMessageDialogProperties(messageText); + + if (dialogType == JsDialogType.Alert) { + jsDialogOpener.Alert(type, "Browser Message", text, success => { + callbackAdapter.Continue(callback, success); + callbackAdapter.Dispose(callback); + }); + } + else if (dialogType == JsDialogType.Confirm) { + jsDialogOpener.Confirm(type, "Browser Confirmation", text, success => { + callbackAdapter.Continue(callback, success); + callbackAdapter.Dispose(callback); + }); + } + else if (dialogType == JsDialogType.Prompt) { + jsDialogOpener.Prompt(type, "Browser Prompt", text, (success, input) => { + callbackAdapter.Continue(callback, success, input); + callbackAdapter.Dispose(callback); + }); + } + else { + return false; + } + + return true; + } + + public bool OnBeforeUnloadDialog(IDisposable callback) { + callback.Dispose(); + return false; + } + } +} diff --git a/windows/TweetDuck/Browser/Base/CefBrowserComponent.cs b/windows/TweetDuck/Browser/Base/CefBrowserComponent.cs index 58f0e93b..9a059ddf 100644 --- a/windows/TweetDuck/Browser/Base/CefBrowserComponent.cs +++ b/windows/TweetDuck/Browser/Base/CefBrowserComponent.cs @@ -1,7 +1,6 @@ using CefSharp.WinForms; using TweetDuck.Utils; using TweetImpl.CefSharp.Component; -using TweetLib.Browser.Base; using TweetLib.Browser.CEF.Utils; using TweetLib.Core; @@ -11,13 +10,8 @@ sealed class CefBrowserComponent : BrowserComponentBase { public override string CacheFolder => CefUtils.GetCacheFolder(App.StoragePath); - public CefBrowserComponent(ChromiumWebBrowser browser, CreateContextMenu createContextMenu = null, bool autoReload = true) : base(browser, createContextMenu ?? DefaultContextMenuFactory, PopupHandler.Instance, autoReload) { + public CefBrowserComponent(ChromiumWebBrowser browser, CreateContextMenu createContextMenu = null, bool autoReload = true) : base(browser, createContextMenu ?? DefaultContextMenuFactory, new JsDialogOpener(browser), PopupHandler.Instance, autoReload) { browser.SetupZoomEvents(); } - - public override void Setup(BrowserSetup setup) { - base.Setup(setup); - browser.JsDialogHandler = new CustomJsDialogHandler(); - } } } diff --git a/windows/TweetDuck/Browser/Base/CustomJsDialogHandler.cs b/windows/TweetDuck/Browser/Base/CustomJsDialogHandler.cs deleted file mode 100644 index e6cf67c8..00000000 --- a/windows/TweetDuck/Browser/Base/CustomJsDialogHandler.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System.Drawing; -using System.Windows.Forms; -using CefSharp; -using CefSharp.WinForms; -using TweetDuck.Controls; -using TweetDuck.Dialogs; -using TweetDuck.Utils; - -namespace TweetDuck.Browser.Base { - sealed class CustomJsDialogHandler : IJsDialogHandler { - private static FormMessage CreateMessageForm(string caption, string text) { - MessageBoxIcon icon = MessageBoxIcon.None; - int pipe = text.IndexOf('|'); - - if (pipe != -1) { - icon = text.Substring(0, pipe) switch { - "error" => MessageBoxIcon.Error, - "warning" => MessageBoxIcon.Warning, - "info" => MessageBoxIcon.Information, - "question" => MessageBoxIcon.Question, - _ => MessageBoxIcon.None - }; - - if (icon != MessageBoxIcon.None) { - text = text.Substring(pipe + 1); - } - } - - return new FormMessage(caption, text, icon); - } - - bool IJsDialogHandler.OnJSDialog(IWebBrowser browserControl, IBrowser browser, string originUrl, CefJsDialogType dialogType, string messageText, string defaultPromptText, IJsDialogCallback callback, ref bool suppressMessage) { - var control = (ChromiumWebBrowser) browserControl; - - control.InvokeSafe(() => { - FormMessage form; - TextBox input = null; - - if (dialogType == CefJsDialogType.Alert) { - form = CreateMessageForm("Browser Message", messageText); - form.AddButton(FormMessage.OK, ControlType.Accept | ControlType.Focused); - } - else if (dialogType == CefJsDialogType.Confirm) { - form = CreateMessageForm("Browser Confirmation", messageText); - form.AddButton(FormMessage.No, DialogResult.No, ControlType.Cancel); - form.AddButton(FormMessage.Yes, ControlType.Focused); - } - else if (dialogType == CefJsDialogType.Prompt) { - form = CreateMessageForm("Browser Prompt", messageText); - form.AddButton(FormMessage.Cancel, DialogResult.Cancel, ControlType.Cancel); - form.AddButton(FormMessage.OK, ControlType.Accept | ControlType.Focused); - - float dpiScale = form.GetDPIScale(); - int inputPad = form.HasIcon ? 43 : 0; - - input = new TextBox { - Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom, - Font = SystemFonts.MessageBoxFont, - Location = new Point(BrowserUtils.Scale(22 + inputPad, dpiScale), form.ActionPanelY - BrowserUtils.Scale(46, dpiScale)), - Size = new Size(form.ClientSize.Width - BrowserUtils.Scale(44 + inputPad, dpiScale), BrowserUtils.Scale(23, dpiScale)) - }; - - form.Controls.Add(input); - form.ActiveControl = input; - form.Height += input.Size.Height + input.Margin.Vertical; - } - else { - callback.Continue(false); - return; - } - - bool success = form.ShowDialog() == DialogResult.OK; - - if (input == null) { - callback.Continue(success); - } - else { - callback.Continue(success, input.Text); - input.Dispose(); - } - - callback.Dispose(); - form.Dispose(); - }); - - return true; - } - - bool IJsDialogHandler.OnBeforeUnloadDialog(IWebBrowser browserControl, IBrowser browser, string messageText, bool isReload, IJsDialogCallback callback) { - callback.Dispose(); - return false; - } - - void IJsDialogHandler.OnResetDialogState(IWebBrowser browserControl, IBrowser browser) {} - void IJsDialogHandler.OnDialogClosed(IWebBrowser browserControl, IBrowser browser) {} - } -} diff --git a/windows/TweetDuck/Browser/Base/FileDialogHandler.cs b/windows/TweetDuck/Browser/Base/FileDialogHandler.cs deleted file mode 100644 index e1c1a631..00000000 --- a/windows/TweetDuck/Browser/Base/FileDialogHandler.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Windows.Forms; -using CefSharp; -using TweetLib.Core; -using TweetLib.Utils.Static; - -namespace TweetDuck.Browser.Base { - sealed class FileDialogHandler : IDialogHandler { - public bool OnFileDialog(IWebBrowser browserControl, IBrowser browser, CefFileDialogMode mode, CefFileDialogFlags flags, string title, string defaultFilePath, List<string> acceptFilters, int selectedAcceptFilter, IFileDialogCallback callback) { - if (mode == CefFileDialogMode.Open || mode == CefFileDialogMode.OpenMultiple) { - string allFilters = string.Join(";", acceptFilters.SelectMany(ParseFileType).Where(filter => !string.IsNullOrEmpty(filter)).Select(filter => "*" + filter)); - - using OpenFileDialog dialog = new OpenFileDialog { - AutoUpgradeEnabled = true, - DereferenceLinks = true, - Multiselect = mode == CefFileDialogMode.OpenMultiple, - Title = "Open Files", - Filter = $"All Supported Formats ({allFilters})|{allFilters}|All Files (*.*)|*.*" - }; - - if (dialog.ShowDialog() == DialogResult.OK) { - string ext = Path.GetExtension(dialog.FileName)?.ToLower(); - callback.Continue(acceptFilters.FindIndex(filter => ParseFileType(filter).Contains(ext)), dialog.FileNames.ToList()); - } - else { - callback.Cancel(); - } - - callback.Dispose(); - return true; - } - else { - callback.Dispose(); - return false; - } - } - - private static IEnumerable<string> ParseFileType(string type) { - if (string.IsNullOrEmpty(type)) { - return StringUtils.EmptyArray; - } - - if (type[0] == '.') { - return new string[] { type }; - } - - string[] extensions = type switch { - "image/jpeg" => new string[] { ".jpg", ".jpeg" }, - "image/png" => new string[] { ".png" }, - "image/gif" => new string[] { ".gif" }, - "image/webp" => new string[] { ".webp" }, - "video/mp4" => new string[] { ".mp4" }, - "video/quicktime" => new string[] { ".mov", ".qt" }, - _ => StringUtils.EmptyArray - }; - - if (extensions.Length == 0) { - App.Logger.Warn("Unknown file type: " + type); - Debugger.Break(); - } - - return extensions; - } - } -} diff --git a/windows/TweetDuck/Browser/Base/JsDialogOpener.cs b/windows/TweetDuck/Browser/Base/JsDialogOpener.cs new file mode 100644 index 00000000..26fde46b --- /dev/null +++ b/windows/TweetDuck/Browser/Base/JsDialogOpener.cs @@ -0,0 +1,74 @@ +using System; +using System.Drawing; +using System.Windows.Forms; +using TweetDuck.Controls; +using TweetDuck.Dialogs; +using TweetDuck.Utils; +using TweetLib.Browser.CEF.Dialogs; +using TweetLib.Browser.CEF.Interfaces; + +namespace TweetDuck.Browser.Base { + sealed class JsDialogOpener : IJsDialogOpener { + private static FormMessage CreateMessageForm(MessageDialogType type, string title, string text) { + return new FormMessage(title, text, type switch { + MessageDialogType.Error => MessageBoxIcon.Error, + MessageDialogType.Warning => MessageBoxIcon.Warning, + MessageDialogType.Information => MessageBoxIcon.Information, + MessageDialogType.Question => MessageBoxIcon.Question, + _ => MessageBoxIcon.None + }); + } + + private readonly Control control; + + public JsDialogOpener(Control control) { + this.control = control; + } + + public void Alert(MessageDialogType type, string title, string message, Action<bool> callback) { + control.InvokeSafe(() => { + using FormMessage form = CreateMessageForm(type, title, message); + form.AddButton(FormMessage.OK, ControlType.Accept | ControlType.Focused); + + bool success = form.ShowDialog() == DialogResult.OK; + callback(success); + }); + } + + public void Confirm(MessageDialogType type, string title, string message, Action<bool> callback) { + control.InvokeSafe(() => { + using FormMessage form = CreateMessageForm(type, title, message); + form.AddButton(FormMessage.No, DialogResult.No, ControlType.Cancel); + form.AddButton(FormMessage.Yes, ControlType.Focused); + + bool success = form.ShowDialog() == DialogResult.OK; + callback(success); + }); + } + + public void Prompt(MessageDialogType type, string title, string message, Action<bool, string> callback) { + control.InvokeSafe(() => { + using FormMessage form = CreateMessageForm(type, title, message); + form.AddButton(FormMessage.Cancel, DialogResult.Cancel, ControlType.Cancel); + form.AddButton(FormMessage.OK, ControlType.Accept | ControlType.Focused); + + float dpiScale = form.GetDPIScale(); + int inputPad = form.HasIcon ? 43 : 0; + + using var input = new TextBox { + Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom, + Font = SystemFonts.MessageBoxFont, + Location = new Point(BrowserUtils.Scale(22 + inputPad, dpiScale), form.ActionPanelY - BrowserUtils.Scale(46, dpiScale)), + Size = new Size(form.ClientSize.Width - BrowserUtils.Scale(44 + inputPad, dpiScale), BrowserUtils.Scale(23, dpiScale)) + }; + + form.Controls.Add(input); + form.ActiveControl = input; + form.Height += input.Size.Height + input.Margin.Vertical; + + bool success = form.ShowDialog() == DialogResult.OK; + callback(success, input.Text); + }); + } + } +} diff --git a/windows/TweetDuck/Browser/TweetDeckBrowser.cs b/windows/TweetDuck/Browser/TweetDeckBrowser.cs index 4c0ffb63..b62bf45c 100644 --- a/windows/TweetDuck/Browser/TweetDeckBrowser.cs +++ b/windows/TweetDuck/Browser/TweetDeckBrowser.cs @@ -40,7 +40,6 @@ public bool IsTweetDeckWebsite { public TweetDeckBrowser(FormBrowser owner, PluginManager pluginManager, ITweetDeckInterface tweetDeckInterface, UpdateChecker updateChecker) { this.browser = new ChromiumWebBrowser(TwitterUrls.TweetDeck) { - DialogHandler = new FileDialogHandler(), KeyboardHandler = new CustomKeyboardHandler(owner) }; diff --git a/windows/TweetDuck/TweetDuck.csproj b/windows/TweetDuck/TweetDuck.csproj index b1294302..b8efd644 100644 --- a/windows/TweetDuck/TweetDuck.csproj +++ b/windows/TweetDuck/TweetDuck.csproj @@ -105,8 +105,7 @@ <Compile Include="Browser\Base\ContextMenuBrowser.cs" /> <Compile Include="Browser\Base\ContextMenuNotification.cs" /> <Compile Include="Browser\Base\CustomKeyboardHandler.cs" /> - <Compile Include="Browser\Base\FileDialogHandler.cs" /> - <Compile Include="Browser\Base\CustomJsDialogHandler.cs" /> + <Compile Include="Browser\Base\JsDialogOpener.cs" /> <Compile Include="Browser\Base\PopupHandler.cs" /> <Compile Include="Browser\Notification\FormNotificationExample.cs"> <SubType>Form</SubType> diff --git a/windows/TweetImpl.CefSharp/Adapters/CefFileDialogCallbackAdapter.cs b/windows/TweetImpl.CefSharp/Adapters/CefFileDialogCallbackAdapter.cs new file mode 100644 index 00000000..49833b41 --- /dev/null +++ b/windows/TweetImpl.CefSharp/Adapters/CefFileDialogCallbackAdapter.cs @@ -0,0 +1,23 @@ +using System.Linq; +using CefSharp; +using TweetLib.Browser.CEF.Interfaces; + +namespace TweetImpl.CefSharp.Adapters { + sealed class CefFileDialogCallbackAdapter : IFileDialogCallbackAdapter<IFileDialogCallback> { + public static CefFileDialogCallbackAdapter Instance { get; } = new CefFileDialogCallbackAdapter(); + + private CefFileDialogCallbackAdapter() {} + + public void Continue(IFileDialogCallback callback, int selectedAcceptFilter, string[] filePaths) { + callback.Continue(selectedAcceptFilter, filePaths.ToList()); + } + + public void Cancel(IFileDialogCallback callback) { + callback.Cancel(); + } + + public void Dispose(IFileDialogCallback callback) { + callback.Dispose(); + } + } +} diff --git a/windows/TweetImpl.CefSharp/Adapters/CefJsDialogCallbackAdapter.cs b/windows/TweetImpl.CefSharp/Adapters/CefJsDialogCallbackAdapter.cs new file mode 100644 index 00000000..61e1c04d --- /dev/null +++ b/windows/TweetImpl.CefSharp/Adapters/CefJsDialogCallbackAdapter.cs @@ -0,0 +1,23 @@ +using CefSharp; +using TweetLib.Browser.CEF.Interfaces; + +namespace TweetImpl.CefSharp.Adapters { + sealed class CefJsDialogCallbackAdapter : IJsDialogCallbackAdapter<IJsDialogCallback> { + public static CefJsDialogCallbackAdapter Instance { get; } = new CefJsDialogCallbackAdapter(); + + private CefJsDialogCallbackAdapter() {} + + public void Continue(IJsDialogCallback callback, bool success, string userInput = null) { + if (userInput == null) { + callback.Continue(success); + } + else { + callback.Continue(success, userInput); + } + } + + public void Dispose(IJsDialogCallback callback) { + callback.Dispose(); + } + } +} diff --git a/windows/TweetImpl.CefSharp/Component/BrowserComponentBase.cs b/windows/TweetImpl.CefSharp/Component/BrowserComponentBase.cs index 372bdb30..fd4cb260 100644 --- a/windows/TweetImpl.CefSharp/Component/BrowserComponentBase.cs +++ b/windows/TweetImpl.CefSharp/Component/BrowserComponentBase.cs @@ -14,18 +14,20 @@ public abstract class BrowserComponentBase : BrowserComponent<IFrame, IRequest> public ResourceHandlerRegistry<IResourceHandler> ResourceHandlerRegistry { get; } = new ResourceHandlerRegistry<IResourceHandler>(CefResourceHandlerFactory.Instance); - protected readonly ChromiumWebBrowser browser; + private readonly ChromiumWebBrowser browser; private readonly CreateContextMenu createContextMenu; + private readonly IJsDialogOpener jsDialogOpener; private readonly IPopupHandler popupHandler; private readonly bool autoReload; - protected BrowserComponentBase(ChromiumWebBrowser browser, CreateContextMenu createContextMenu, IPopupHandler popupHandler, bool autoReload) : base(new CefBrowserAdapter(browser), CefAdapter.Instance, CefFrameAdapter.Instance, CefRequestAdapter.Instance) { + protected BrowserComponentBase(ChromiumWebBrowser browser, CreateContextMenu createContextMenu, IJsDialogOpener jsDialogOpener, IPopupHandler popupHandler, bool autoReload) : base(new CefBrowserAdapter(browser), CefAdapter.Instance, CefFrameAdapter.Instance, CefRequestAdapter.Instance) { this.browser = browser; this.browser.LoadingStateChanged += OnLoadingStateChanged; this.browser.LoadError += OnLoadError; this.browser.FrameLoadStart += OnFrameLoadStart; this.browser.FrameLoadEnd += OnFrameLoadEnd; this.createContextMenu = createContextMenu; + this.jsDialogOpener = jsDialogOpener; this.popupHandler = popupHandler; this.autoReload = autoReload; } @@ -34,8 +36,10 @@ public override void Setup(BrowserSetup setup) { var lifeSpanHandler = new CefLifeSpanHandler(popupHandler); var requestHandler = new CefRequestHandler(lifeSpanHandler, autoReload); + browser.DialogHandler = new CefFileDialogHandler(); browser.DragHandler = new CefDragHandler(requestHandler, this); browser.LifeSpanHandler = lifeSpanHandler; + browser.JsDialogHandler = new CefJsDialogHandler(jsDialogOpener); browser.MenuHandler = createContextMenu(setup.ContextMenuHandler); browser.RequestHandler = requestHandler; browser.ResourceRequestHandlerFactory = new CefResourceRequestHandlerFactory(setup.ResourceRequestHandler, ResourceHandlerRegistry); diff --git a/windows/TweetImpl.CefSharp/Dialogs/FileDialogOpener.cs b/windows/TweetImpl.CefSharp/Dialogs/FileDialogOpener.cs new file mode 100644 index 00000000..b4a9ccf3 --- /dev/null +++ b/windows/TweetImpl.CefSharp/Dialogs/FileDialogOpener.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; +using TweetLib.Browser.CEF.Interfaces; + +namespace TweetImpl.CefSharp.Dialogs { + sealed class FileDialogOpener : IFileDialogOpener { + public static FileDialogOpener Instance { get; } = new FileDialogOpener(); + + private FileDialogOpener() {} + + public void OpenFile(string title, bool multiple, List<string> supportedExtensions, Action<string[]> onAccepted, Action onCancelled) { + string supportedFormatsFilter = string.Join(";", supportedExtensions.Select(filter => "*" + filter)); + + using OpenFileDialog dialog = new OpenFileDialog { + AutoUpgradeEnabled = true, + DereferenceLinks = true, + Multiselect = multiple, + Title = title, + Filter = $"All Supported Formats ({supportedFormatsFilter})|{supportedFormatsFilter}|All Files (*.*)|*.*" + }; + + if (dialog.ShowDialog() == DialogResult.OK) { + onAccepted(dialog.FileNames); + } + else { + onCancelled(); + } + } + } +} diff --git a/windows/TweetImpl.CefSharp/Handlers/CefFileDialogHandler.cs b/windows/TweetImpl.CefSharp/Handlers/CefFileDialogHandler.cs new file mode 100644 index 00000000..fe2e5256 --- /dev/null +++ b/windows/TweetImpl.CefSharp/Handlers/CefFileDialogHandler.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using CefSharp; +using TweetImpl.CefSharp.Adapters; +using TweetImpl.CefSharp.Dialogs; +using TweetLib.Browser.CEF.Dialogs; +using TweetLib.Browser.CEF.Logic; +using static TweetLib.Browser.CEF.Dialogs.FileDialogType; + +namespace TweetImpl.CefSharp.Handlers { + sealed class CefFileDialogHandler : IDialogHandler { + private readonly DialogHandlerLogic<IFileDialogCallback> logic; + + public CefFileDialogHandler() { + this.logic = new DialogHandlerLogic<IFileDialogCallback>(FileDialogOpener.Instance, CefFileDialogCallbackAdapter.Instance); + } + + public bool OnFileDialog(IWebBrowser chromiumWebBrowser, IBrowser browser, CefFileDialogMode mode, CefFileDialogFlags flags, string title, string defaultFilePath, List<string> acceptFilters, int selectedAcceptFilter, IFileDialogCallback callback) { + return logic.OnFileDialog(ConvertDialogType(mode), acceptFilters, callback); + } + + private static FileDialogType ConvertDialogType(CefFileDialogMode mode) { + return mode switch { + CefFileDialogMode.Open => Open, + CefFileDialogMode.OpenMultiple => OpenMultiple, + _ => Other + }; + } + } +} diff --git a/windows/TweetImpl.CefSharp/Handlers/CefJsDialogHandler.cs b/windows/TweetImpl.CefSharp/Handlers/CefJsDialogHandler.cs new file mode 100644 index 00000000..a6faa1d1 --- /dev/null +++ b/windows/TweetImpl.CefSharp/Handlers/CefJsDialogHandler.cs @@ -0,0 +1,37 @@ +using System.Diagnostics.CodeAnalysis; +using CefSharp; +using TweetImpl.CefSharp.Adapters; +using TweetLib.Browser.CEF.Dialogs; +using TweetLib.Browser.CEF.Interfaces; +using TweetLib.Browser.CEF.Logic; + +namespace TweetImpl.CefSharp.Handlers { + sealed class CefJsDialogHandler : IJsDialogHandler { + private readonly JsDialogHandlerLogic<IJsDialogCallback> logic; + + public CefJsDialogHandler(IJsDialogOpener jsDialogOpener) { + this.logic = new JsDialogHandlerLogic<IJsDialogCallback>(jsDialogOpener, CefJsDialogCallbackAdapter.Instance); + } + + [SuppressMessage("ReSharper", "RedundantAssignment")] + public bool OnJSDialog(IWebBrowser chromiumWebBrowser, IBrowser browser, string originUrl, CefJsDialogType dialogType, string messageText, string defaultPromptText, IJsDialogCallback callback, ref bool suppressMessage) { + return logic.OnJSDialog(ConvertDialogType(dialogType), messageText, callback, out suppressMessage); + } + + public bool OnBeforeUnloadDialog(IWebBrowser chromiumWebBrowser, IBrowser browser, string messageText, bool isReload, IJsDialogCallback callback) { + return logic.OnBeforeUnloadDialog(callback); + } + + public void OnResetDialogState(IWebBrowser chromiumWebBrowser, IBrowser browser) {} + public void OnDialogClosed(IWebBrowser chromiumWebBrowser, IBrowser browser) {} + + private static JsDialogType ConvertDialogType(CefJsDialogType type) { + return type switch { + CefJsDialogType.Alert => JsDialogType.Alert, + CefJsDialogType.Confirm => JsDialogType.Confirm, + CefJsDialogType.Prompt => JsDialogType.Prompt, + _ => JsDialogType.Unknown + }; + } + } +} diff --git a/windows/TweetImpl.CefSharp/TweetImpl.CefSharp.csproj b/windows/TweetImpl.CefSharp/TweetImpl.CefSharp.csproj index 27b50baf..18e83a70 100644 --- a/windows/TweetImpl.CefSharp/TweetImpl.CefSharp.csproj +++ b/windows/TweetImpl.CefSharp/TweetImpl.CefSharp.csproj @@ -69,15 +69,20 @@ <Compile Include="Adapters\CefBrowserAdapter.cs" /> <Compile Include="Adapters\CefDragDataAdapter.cs" /> <Compile Include="Adapters\CefErrorCodeAdapter.cs" /> + <Compile Include="Adapters\CefFileDialogCallbackAdapter.cs" /> <Compile Include="Adapters\CefFrameAdapter.cs" /> + <Compile Include="Adapters\CefJsDialogCallbackAdapter.cs" /> <Compile Include="Adapters\CefMenuModelAdapter.cs" /> <Compile Include="Adapters\CefRequestAdapter.cs" /> <Compile Include="Adapters\CefResponseAdapter.cs" /> <Compile Include="Component\BrowserComponentBase.cs" /> + <Compile Include="Dialogs\FileDialogOpener.cs" /> <Compile Include="Handlers\CefByteArrayResourceHandler.cs" /> <Compile Include="Handlers\CefContextMenuHandler.cs" /> <Compile Include="Handlers\CefDownloadRequestClient.cs" /> <Compile Include="Handlers\CefDragHandler.cs" /> + <Compile Include="Handlers\CefFileDialogHandler.cs" /> + <Compile Include="Handlers\CefJsDialogHandler.cs" /> <Compile Include="Handlers\CefLifeSpanHandler.cs" /> <Compile Include="Handlers\CefRequestHandler.cs" /> <Compile Include="Handlers\CefResourceHandlerFactory.cs" />