diff --git a/lib/TweetLib.Browser.CEF/Component/BrowserComponent.cs b/lib/TweetLib.Browser.CEF/Component/BrowserComponent.cs index 3680e12b..3a16447f 100644 --- a/lib/TweetLib.Browser.CEF/Component/BrowserComponent.cs +++ b/lib/TweetLib.Browser.CEF/Component/BrowserComponent.cs @@ -1,12 +1,14 @@ using System; +using System.IO; using TweetLib.Browser.Base; +using TweetLib.Browser.CEF.Data; using TweetLib.Browser.CEF.Interfaces; using TweetLib.Browser.Events; using TweetLib.Browser.Interfaces; using TweetLib.Utils.Static; namespace TweetLib.Browser.CEF.Component { - public abstract class BrowserComponent<TFrame> : IBrowserComponent where TFrame : IDisposable { + public abstract class BrowserComponent<TFrame, TRequest> : IBrowserComponent where TFrame : IDisposable { public bool Ready { get; private set; } public string Url => browser.Url; @@ -16,22 +18,25 @@ public abstract class BrowserComponent<TFrame> : IBrowserComponent where TFrame public event EventHandler<PageLoadEventArgs>? PageLoadStart; public event EventHandler<PageLoadEventArgs>? PageLoadEnd; - private readonly IBrowserWrapper<TFrame> browser; + private readonly IBrowserWrapper<TFrame, TRequest> browser; + private readonly ICefAdapter cefAdapter; private readonly IFrameAdapter<TFrame> frameAdapter; + private readonly IRequestAdapter<TRequest> requestAdapter; - protected BrowserComponent(IBrowserWrapper<TFrame> browser, IFrameAdapter<TFrame> frameAdapter) { + protected BrowserComponent(IBrowserWrapper<TFrame, TRequest> browser, ICefAdapter cefAdapter, IFrameAdapter<TFrame> frameAdapter, IRequestAdapter<TRequest> requestAdapter) { this.browser = browser; + this.cefAdapter = cefAdapter; this.frameAdapter = frameAdapter; + this.requestAdapter = requestAdapter; } public abstract void Setup(BrowserSetup setup); public abstract void AttachBridgeObject(string name, object bridge); - public abstract void DownloadFile(string url, string path, Action? onSuccess, Action<Exception>? onError); private sealed class BrowserLoadedEventArgsImpl : BrowserLoadedEventArgs { - private readonly IBrowserWrapper<TFrame> browser; + private readonly IBrowserWrapper<TFrame, TRequest> browser; - public BrowserLoadedEventArgsImpl(IBrowserWrapper<TFrame> browser) { + public BrowserLoadedEventArgsImpl(IBrowserWrapper<TFrame, TRequest> browser) { this.browser = browser; } @@ -82,5 +87,25 @@ public void RunScript(string identifier, string script) { using TFrame frame = browser.MainFrame; frameAdapter.ExecuteJavaScriptAsync(frame, script, identifier, 1); } + + public void DownloadFile(string url, string path, Action? onSuccess, Action<Exception>? onError) { + cefAdapter.RunOnUiThread(() => { + var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read); + + try { + var request = browser.CreateGetRequest(); + requestAdapter.SetUrl(request, url); + requestAdapter.SetMethod(request, "GET"); + requestAdapter.SetReferrer(request, Url); + requestAdapter.SetAllowStoredCredentials(request); + + using TFrame frame = browser.MainFrame; + browser.RequestDownload(frame, request, new DownloadCallbacks(fileStream, onSuccess, onError)); + } catch (Exception e) { + fileStream.Dispose(); + onError?.Invoke(e); + } + }); + } } } diff --git a/lib/TweetLib.Browser.CEF/Data/DownloadCallbacks.cs b/lib/TweetLib.Browser.CEF/Data/DownloadCallbacks.cs new file mode 100644 index 00000000..241aecd1 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Data/DownloadCallbacks.cs @@ -0,0 +1,33 @@ +using System; +using System.IO; + +namespace TweetLib.Browser.CEF.Data { + public sealed class DownloadCallbacks { + internal bool HasData { get; private set; } + + private readonly FileStream fileStream; + private readonly Action? onSuccess; + private readonly Action<Exception>? onError; + + internal DownloadCallbacks(FileStream fileStream, Action? onSuccess, Action<Exception>? onError) { + this.fileStream = fileStream; + this.onSuccess = onSuccess; + this.onError = onError; + } + + internal void OnData(Stream data) { + data.CopyTo(fileStream); + HasData |= fileStream.Position > 0; + } + + internal void OnSuccess() { + fileStream.Dispose(); + onSuccess?.Invoke(); + } + + internal void OnError(Exception e) { + fileStream.Dispose(); + onError?.Invoke(e); + } + } +} diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IBrowserWrapper.cs b/lib/TweetLib.Browser.CEF/Interfaces/IBrowserWrapper.cs index 6bbdf2c1..616b2e57 100644 --- a/lib/TweetLib.Browser.CEF/Interfaces/IBrowserWrapper.cs +++ b/lib/TweetLib.Browser.CEF/Interfaces/IBrowserWrapper.cs @@ -1,10 +1,13 @@ using System; +using TweetLib.Browser.CEF.Data; namespace TweetLib.Browser.CEF.Interfaces { - public interface IBrowserWrapper<TFrame> where TFrame : IDisposable { + public interface IBrowserWrapper<TFrame, TRequest> where TFrame : IDisposable { string Url { get; } TFrame MainFrame { get; } void AddWordToDictionary(string word); + TRequest CreateGetRequest(); + void RequestDownload(TFrame frame, TRequest request, DownloadCallbacks callbacks); } } diff --git a/lib/TweetLib.Browser.CEF/Interfaces/ICefAdapter.cs b/lib/TweetLib.Browser.CEF/Interfaces/ICefAdapter.cs new file mode 100644 index 00000000..74c4cf39 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Interfaces/ICefAdapter.cs @@ -0,0 +1,7 @@ +using System; + +namespace TweetLib.Browser.CEF.Interfaces { + public interface ICefAdapter { + void RunOnUiThread(Action action); + } +} diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IMenuModelAdapter.cs b/lib/TweetLib.Browser.CEF/Interfaces/IMenuModelAdapter.cs new file mode 100644 index 00000000..87ab1929 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Interfaces/IMenuModelAdapter.cs @@ -0,0 +1,16 @@ +namespace TweetLib.Browser.CEF.Interfaces { + public interface IMenuModelAdapter<T> { + int GetItemCount(T model); + + void AddCommand(T model, int command, string name); + int GetCommandAt(T model, int index); + + void AddCheckCommand(T model, int command, string name); + void SetChecked(T model, int command, bool isChecked); + + void AddSeparator(T model); + bool IsSeparatorAt(T model, int index); + + void RemoveAt(T model, int index); + } +} diff --git a/lib/TweetLib.Browser.CEF/Interfaces/IRequestAdapter.cs b/lib/TweetLib.Browser.CEF/Interfaces/IRequestAdapter.cs index 88393ad7..04fb54d9 100644 --- a/lib/TweetLib.Browser.CEF/Interfaces/IRequestAdapter.cs +++ b/lib/TweetLib.Browser.CEF/Interfaces/IRequestAdapter.cs @@ -8,6 +8,8 @@ public interface IRequestAdapter<T> { void SetUrl(T request, string url); + void SetMethod(T request, string method); + bool IsTransitionForwardBack(T request); bool IsCspReport(T request); @@ -15,5 +17,9 @@ public interface IRequestAdapter<T> { ResourceType GetResourceType(T request); void SetHeader(T request, string header, string value); + + void SetReferrer(T request, string referrer); + + void SetAllowStoredCredentials(T request); } } diff --git a/lib/TweetLib.Browser.CEF/Logic/ByteArrayResourceHandlerLogic.cs b/lib/TweetLib.Browser.CEF/Logic/ByteArrayResourceHandlerLogic.cs index 268d3e02..71cbda94 100644 --- a/lib/TweetLib.Browser.CEF/Logic/ByteArrayResourceHandlerLogic.cs +++ b/lib/TweetLib.Browser.CEF/Logic/ByteArrayResourceHandlerLogic.cs @@ -4,10 +4,12 @@ using TweetLib.Browser.CEF.Interfaces; namespace TweetLib.Browser.CEF.Logic { - [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] - public sealed class ByteArrayResourceHandlerLogic<TResponse> { + public abstract class ByteArrayResourceHandlerLogic { public delegate void WriteToOut<T>(T dataOut, byte[] dataIn, int position, int length); + } + [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] + public sealed class ByteArrayResourceHandlerLogic<TResponse> : ByteArrayResourceHandlerLogic { private readonly ByteArrayResource resource; private readonly IResponseAdapter<TResponse> responseAdapter; diff --git a/lib/TweetLib.Browser.CEF/Logic/ContextMenuLogic.cs b/lib/TweetLib.Browser.CEF/Logic/ContextMenuLogic.cs new file mode 100644 index 00000000..62ba5310 --- /dev/null +++ b/lib/TweetLib.Browser.CEF/Logic/ContextMenuLogic.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using TweetLib.Browser.CEF.Data; +using TweetLib.Browser.CEF.Interfaces; +using TweetLib.Browser.Contexts; +using TweetLib.Browser.Interfaces; + +namespace TweetLib.Browser.CEF.Logic { + public abstract class ContextMenuLogic { + protected const int CommandCustomFirst = 220; + protected const int CommandCustomLast = 250; + + protected static readonly HashSet<int> AllowedCommands = new () { + -1, // NotFound + 110, // Undo + 111, // Redo + 112, // Cut + 113, // Copy + 114, // Paste + 115, // Delete + 116, // SelectAll + 200, // SpellCheckSuggestion0 + 201, // SpellCheckSuggestion1 + 202, // SpellCheckSuggestion2 + 203, // SpellCheckSuggestion3 + 204, // SpellCheckSuggestion4 + 205, // SpellCheckNoSuggestions + 206 // AddToDictionary + }; + + protected sealed class ContextMenuActionRegistry : ContextMenuActionRegistry<int> { + private const int CommandUserFirst = 26500; + + protected override int NextId(int n) { + return CommandUserFirst + 500 + n; + } + } + } + + public sealed class ContextMenuLogic<TModel> : ContextMenuLogic { + private readonly IContextMenuHandler? handler; + private readonly IMenuModelAdapter<TModel> modelAdapter; + private readonly ContextMenuActionRegistry actionRegistry; + + public ContextMenuLogic(IContextMenuHandler? handler, IMenuModelAdapter<TModel> modelAdapter) { + this.handler = handler; + this.modelAdapter = modelAdapter; + this.actionRegistry = new ContextMenuActionRegistry(); + } + + private sealed class ContextMenuBuilder : IContextMenuBuilder { + private readonly IMenuModelAdapter<TModel> modelAdapter; + private readonly ContextMenuActionRegistry actionRegistry; + private readonly TModel model; + + public ContextMenuBuilder(IMenuModelAdapter<TModel> modelAdapter, ContextMenuActionRegistry actionRegistry, TModel model) { + this.model = model; + this.actionRegistry = actionRegistry; + this.modelAdapter = modelAdapter; + } + + public void AddAction(string name, Action action) { + var id = actionRegistry.AddAction(action); + modelAdapter.AddCommand(model, id, name); + } + + public void AddActionWithCheck(string name, bool isChecked, Action action) { + var id = actionRegistry.AddAction(action); + modelAdapter.AddCheckCommand(model, id, name); + modelAdapter.SetChecked(model, id, isChecked); + } + + public void AddSeparator() { + int count = modelAdapter.GetItemCount(model); + if (count > 0 && !modelAdapter.IsSeparatorAt(model, count - 1)) { // do not add separators if there is nothing to separate + modelAdapter.AddSeparator(model); + } + } + + public void RemoveSeparatorIfLast(TModel model) { + int count = modelAdapter.GetItemCount(model); + if (count > 0 && modelAdapter.IsSeparatorAt(model, count - 1)) { + modelAdapter.RemoveAt(model, count - 1); + } + } + } + + public void OnBeforeContextMenu(TModel model, Context context) { + for (int i = modelAdapter.GetItemCount(model) - 1; i >= 0; i--) { + int command = modelAdapter.GetCommandAt(model, i); + + if (!(AllowedCommands.Contains(command) || command is >= CommandCustomFirst and <= CommandCustomLast)) { + modelAdapter.RemoveAt(model, i); + } + } + + for (int i = modelAdapter.GetItemCount(model) - 2; i >= 0; i--) { + if (modelAdapter.IsSeparatorAt(model, i) && modelAdapter.IsSeparatorAt(model, i + 1)) { + modelAdapter.RemoveAt(model, i); + } + } + + if (modelAdapter.GetItemCount(model) > 0 && modelAdapter.IsSeparatorAt(model, 0)) { + modelAdapter.RemoveAt(model, 0); + } + + var builder = new ContextMenuBuilder(modelAdapter, actionRegistry, model); + builder.AddSeparator(); + handler?.Show(builder, context); + builder.RemoveSeparatorIfLast(model); + } + + public bool OnContextMenuCommand(int commandId) { + return actionRegistry.Execute(commandId); + } + + public void OnContextMenuDismissed() { + actionRegistry.Clear(); + } + + public bool RunContextMenu() { + return false; + } + } +} diff --git a/lib/TweetLib.Browser.CEF/Logic/DownloadRequestClientLogic.cs b/lib/TweetLib.Browser.CEF/Logic/DownloadRequestClientLogic.cs index 0e9cef28..d1995e49 100644 --- a/lib/TweetLib.Browser.CEF/Logic/DownloadRequestClientLogic.cs +++ b/lib/TweetLib.Browser.CEF/Logic/DownloadRequestClientLogic.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.IO; +using TweetLib.Browser.CEF.Data; namespace TweetLib.Browser.CEF.Logic { public sealed class DownloadRequestClientLogic { @@ -10,24 +11,18 @@ public enum RequestStatus { Failed } - private readonly FileStream fileStream; - private readonly Action? onSuccess; - private readonly Action<Exception>? onError; - + private readonly DownloadCallbacks callbacks; private bool hasFailed; - public DownloadRequestClientLogic(FileStream fileStream, Action? onSuccess, Action<Exception>? onError) { - this.fileStream = fileStream; - this.onSuccess = onSuccess; - this.onError = onError; + public DownloadRequestClientLogic(DownloadCallbacks callbacks) { + this.callbacks = callbacks; } public bool GetAuthCredentials(IDisposable callback) { callback.Dispose(); hasFailed = true; - fileStream.Dispose(); - onError?.Invoke(new Exception("This URL requires authentication.")); + callbacks.OnError(new Exception("This URL requires authentication.")); return false; } @@ -38,10 +33,9 @@ public void OnDownloadData(Stream data) { } try { - data.CopyTo(fileStream); + callbacks.OnData(data); } catch (Exception e) { - fileStream.Dispose(); - onError?.Invoke(e); + callbacks.OnError(e); hasFailed = true; } } @@ -52,20 +46,17 @@ public void OnRequestComplete(RequestStatus status) { return; } - bool isEmpty = fileStream.Position == 0; - fileStream.Dispose(); - switch (status) { - case RequestStatus.Failed: - onError?.Invoke(new Exception("Unknown error.")); + case RequestStatus.Success when callbacks.HasData: + callbacks.OnSuccess(); break; - case RequestStatus.Success when isEmpty: - onError?.Invoke(new Exception("File is empty.")); - return; - case RequestStatus.Success: - onSuccess?.Invoke(); + callbacks.OnError(new Exception("File is empty.")); + break; + + default: + callbacks.OnError(new Exception("Unknown error.")); break; } } diff --git a/windows/TweetDuck/Browser/Base/CefAdapter.cs b/windows/TweetDuck/Browser/Base/CefAdapter.cs new file mode 100644 index 00000000..06aa6dc8 --- /dev/null +++ b/windows/TweetDuck/Browser/Base/CefAdapter.cs @@ -0,0 +1,15 @@ +using System; +using CefSharp; +using TweetLib.Browser.CEF.Interfaces; + +namespace TweetDuck.Browser.Base { + sealed class CefAdapter : ICefAdapter { + public static CefAdapter Instance { get; } = new CefAdapter(); + + private CefAdapter() {} + + public void RunOnUiThread(Action action) { + Cef.UIThreadTaskFactory.StartNew(action); + } + } +} diff --git a/windows/TweetDuck/Browser/Base/CefBrowserAdapter.cs b/windows/TweetDuck/Browser/Base/CefBrowserAdapter.cs index 456d06a4..abccada1 100644 --- a/windows/TweetDuck/Browser/Base/CefBrowserAdapter.cs +++ b/windows/TweetDuck/Browser/Base/CefBrowserAdapter.cs @@ -1,9 +1,10 @@ using CefSharp; using CefSharp.WinForms; +using TweetLib.Browser.CEF.Data; using TweetLib.Browser.CEF.Interfaces; namespace TweetDuck.Browser.Base { - sealed class CefBrowserAdapter : IBrowserWrapper<IFrame> { + sealed class CefBrowserAdapter : IBrowserWrapper<IFrame, IRequest> { public string Url => browser.Address; public IFrame MainFrame => browser.GetMainFrame(); @@ -16,5 +17,14 @@ public CefBrowserAdapter(ChromiumWebBrowser browser) { public void AddWordToDictionary(string word) { browser.AddWordToDictionary(word); } + + public IRequest CreateGetRequest() { + using var frame = MainFrame; + return frame.CreateRequest(false); + } + + public void RequestDownload(IFrame frame, IRequest request, DownloadCallbacks callbacks) { + frame.CreateUrlRequest(request, new CefDownloadRequestClient(callbacks)); + } } } diff --git a/windows/TweetDuck/Browser/Base/CefBrowserComponent.cs b/windows/TweetDuck/Browser/Base/CefBrowserComponent.cs index cd1d2592..d8f7c4c7 100644 --- a/windows/TweetDuck/Browser/Base/CefBrowserComponent.cs +++ b/windows/TweetDuck/Browser/Base/CefBrowserComponent.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using CefSharp; using CefSharp.WinForms; using TweetDuck.Browser.Handling; @@ -11,7 +9,7 @@ using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler; namespace TweetDuck.Browser.Base { - sealed class CefBrowserComponent : BrowserComponent<IFrame> { + sealed class CefBrowserComponent : BrowserComponent<IFrame, IRequest> { public delegate ContextMenuBase CreateContextMenu(IContextMenuHandler handler); private static readonly CreateContextMenu DefaultContextMenuFactory = handler => new ContextMenuBase(handler); @@ -25,7 +23,7 @@ sealed class CefBrowserComponent : BrowserComponent<IFrame> { private CreateContextMenu createContextMenu; - public CefBrowserComponent(ChromiumWebBrowser browser, CreateContextMenu createContextMenu = null, bool autoReload = true) : base(new CefBrowserAdapter(browser), CefFrameAdapter.Instance) { + public CefBrowserComponent(ChromiumWebBrowser browser, CreateContextMenu createContextMenu = null, bool autoReload = true) : base(new CefBrowserAdapter(browser), CefAdapter.Instance, CefFrameAdapter.Instance, CefRequestAdapter.Instance) { this.browser = browser; this.browser.LoadingStateChanged += OnLoadingStateChanged; this.browser.LoadError += OnLoadError; @@ -70,26 +68,5 @@ private void OnFrameLoadStart(object sender, FrameLoadStartEventArgs e) { private void OnFrameLoadEnd(object sender, FrameLoadEndEventArgs e) { base.OnFrameLoadEnd(e.Url, e.Frame); } - - public override void DownloadFile(string url, string path, Action onSuccess, Action<Exception> onError) { - Cef.UIThreadTaskFactory.StartNew(() => { - var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read); - - try { - using IFrame frame = browser.GetMainFrame(); - - var request = frame.CreateRequest(false); - request.Method = "GET"; - request.Url = url; - request.Flags = UrlRequestFlags.AllowStoredCredentials; - request.SetReferrer(Url, ReferrerPolicy.Default); - - frame.CreateUrlRequest(request, new CefDownloadRequestClient(fileStream, onSuccess, onError)); - } catch (Exception e) { - fileStream.Dispose(); - onError?.Invoke(e); - } - }); - } } } diff --git a/windows/TweetDuck/Browser/Base/CefByteArrayResourceHandler.cs b/windows/TweetDuck/Browser/Base/CefByteArrayResourceHandler.cs index 059f49dc..d7874b30 100644 --- a/windows/TweetDuck/Browser/Base/CefByteArrayResourceHandler.cs +++ b/windows/TweetDuck/Browser/Base/CefByteArrayResourceHandler.cs @@ -7,7 +7,7 @@ namespace TweetDuck.Browser.Base { sealed class CefByteArrayResourceHandler : IResourceHandler { - private static readonly ByteArrayResourceHandlerLogic<IResponse>.WriteToOut<Stream> WriteToOut = delegate (Stream dataOut, byte[] dataIn, int position, int length) { + private static readonly ByteArrayResourceHandlerLogic.WriteToOut<Stream> WriteToOut = delegate (Stream dataOut, byte[] dataIn, int position, int length) { dataOut.Write(dataIn, position, length); }; diff --git a/windows/TweetDuck/Browser/Base/CefContextMenuHandler.cs b/windows/TweetDuck/Browser/Base/CefContextMenuHandler.cs new file mode 100644 index 00000000..620fd5f4 --- /dev/null +++ b/windows/TweetDuck/Browser/Base/CefContextMenuHandler.cs @@ -0,0 +1,31 @@ +using CefSharp; +using TweetLib.Browser.CEF.Logic; +using TweetLib.Browser.Contexts; + +namespace TweetDuck.Browser.Base { + abstract class CefContextMenuHandler : IContextMenuHandler { + private readonly ContextMenuLogic<IMenuModel> logic; + + protected CefContextMenuHandler(TweetLib.Browser.Interfaces.IContextMenuHandler handler) { + this.logic = new ContextMenuLogic<IMenuModel>(handler, CefMenuModelAdapter.Instance); + } + + protected abstract Context CreateContext(IContextMenuParams parameters); + + public virtual void OnBeforeContextMenu(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) { + logic.OnBeforeContextMenu(model, CreateContext(parameters)); + } + + public virtual bool OnContextMenuCommand(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags) { + return logic.OnContextMenuCommand((int) commandId); + } + + public virtual void OnContextMenuDismissed(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame) { + logic.OnContextMenuDismissed(); + } + + public bool RunContextMenu(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model, IRunContextMenuCallback callback) { + return logic.RunContextMenu(); + } + } +} diff --git a/windows/TweetDuck/Browser/Base/CefContextMenuModel.cs b/windows/TweetDuck/Browser/Base/CefContextMenuModel.cs deleted file mode 100644 index b4dc2551..00000000 --- a/windows/TweetDuck/Browser/Base/CefContextMenuModel.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using CefSharp; -using TweetLib.Browser.CEF.Data; -using TweetLib.Browser.Contexts; -using TweetLib.Browser.Interfaces; -using TweetLib.Core.Features.TweetDeck; -using TweetLib.Core.Features.Twitter; - -namespace TweetDuck.Browser.Base { - sealed class CefContextMenuModel : IContextMenuBuilder { - private readonly IMenuModel model; - private readonly ContextMenuActionRegistry<CefMenuCommand> actionRegistry; - - public CefContextMenuModel(IMenuModel model, ContextMenuActionRegistry<CefMenuCommand> actionRegistry) { - this.model = model; - this.actionRegistry = actionRegistry; - } - - public void AddAction(string name, Action action) { - var id = actionRegistry.AddAction(action); - model.AddItem(id, name); - } - - public void AddActionWithCheck(string name, bool isChecked, Action action) { - var id = actionRegistry.AddAction(action); - model.AddCheckItem(id, name); - model.SetChecked(id, isChecked); - } - - public void AddSeparator() { - if (model.Count > 0 && model.GetTypeAt(model.Count - 1) != MenuItemType.Separator) { // do not add separators if there is nothing to separate - model.AddSeparator(); - } - } - - public static Context CreateContext(IContextMenuParams parameters, TweetDeckExtraContext extraContext, ImageQuality imageQuality) { - var context = new Context(); - var flags = parameters.TypeFlags; - - var tweet = extraContext?.Tweet; - if (tweet != null && !flags.HasFlag(ContextMenuType.Editable)) { - context.Tweet = tweet; - } - - context.Link = GetLink(parameters, extraContext); - context.Media = GetMedia(parameters, extraContext, imageQuality); - - if (flags.HasFlag(ContextMenuType.Selection)) { - context.Selection = new Selection(parameters.SelectionText, flags.HasFlag(ContextMenuType.Editable)); - } - - return context; - } - - private static Link? GetLink(IContextMenuParams parameters, TweetDeckExtraContext extraContext) { - var link = extraContext?.Link; - if (link != null) { - return link; - } - - if (parameters.TypeFlags.HasFlag(ContextMenuType.Link) && extraContext?.Media == null) { - return new Link(parameters.LinkUrl, parameters.UnfilteredLinkUrl); - } - - return null; - } - - private static Media? GetMedia(IContextMenuParams parameters, TweetDeckExtraContext extraContext, ImageQuality imageQuality) { - var media = extraContext?.Media; - if (media != null) { - return media; - } - - if (parameters.TypeFlags.HasFlag(ContextMenuType.Media) && parameters.HasImageContents) { - return new Media(Media.Type.Image, TwitterUrls.GetMediaLink(parameters.SourceUrl, imageQuality)); - } - - return null; - } - } -} diff --git a/windows/TweetDuck/Browser/Base/CefDownloadRequestClient.cs b/windows/TweetDuck/Browser/Base/CefDownloadRequestClient.cs index 2a3a0954..ef97f03b 100644 --- a/windows/TweetDuck/Browser/Base/CefDownloadRequestClient.cs +++ b/windows/TweetDuck/Browser/Base/CefDownloadRequestClient.cs @@ -1,6 +1,6 @@ -using System; using System.IO; using CefSharp; +using TweetLib.Browser.CEF.Data; using TweetLib.Browser.CEF.Logic; using static TweetLib.Browser.CEF.Logic.DownloadRequestClientLogic.RequestStatus; @@ -8,8 +8,8 @@ namespace TweetDuck.Browser.Base { sealed class CefDownloadRequestClient : UrlRequestClient { private readonly DownloadRequestClientLogic logic; - public CefDownloadRequestClient(FileStream fileStream, Action onSuccess, Action<Exception> onError) { - this.logic = new DownloadRequestClientLogic(fileStream, onSuccess, onError); + public CefDownloadRequestClient(DownloadCallbacks callbacks) { + this.logic = new DownloadRequestClientLogic(callbacks); } protected override bool GetAuthCredentials(bool isProxy, string host, int port, string realm, string scheme, IAuthCallback callback) { diff --git a/windows/TweetDuck/Browser/Base/CefMenuModelAdapter.cs b/windows/TweetDuck/Browser/Base/CefMenuModelAdapter.cs new file mode 100644 index 00000000..b38a5fd7 --- /dev/null +++ b/windows/TweetDuck/Browser/Base/CefMenuModelAdapter.cs @@ -0,0 +1,42 @@ +using CefSharp; +using TweetLib.Browser.CEF.Interfaces; + +namespace TweetDuck.Browser.Base { + sealed class CefMenuModelAdapter : IMenuModelAdapter<IMenuModel> { + public static CefMenuModelAdapter Instance { get; } = new CefMenuModelAdapter(); + + private CefMenuModelAdapter() {} + + public int GetItemCount(IMenuModel model) { + return model.Count; + } + + public void AddCommand(IMenuModel model, int command, string name) { + model.AddItem((CefMenuCommand) command, name); + } + + public int GetCommandAt(IMenuModel model, int index) { + return (int) model.GetCommandIdAt(index); + } + + public void AddCheckCommand(IMenuModel model, int command, string name) { + model.AddCheckItem((CefMenuCommand) command, name); + } + + public void SetChecked(IMenuModel model, int command, bool isChecked) { + model.SetChecked((CefMenuCommand) command, isChecked); + } + + public void AddSeparator(IMenuModel model) { + model.AddSeparator(); + } + + public bool IsSeparatorAt(IMenuModel model, int index) { + return model.GetTypeAt(index) == MenuItemType.Separator; + } + + public void RemoveAt(IMenuModel model, int index) { + model.RemoveAt(index); + } + } +} diff --git a/windows/TweetDuck/Browser/Base/CefRequestAdapter.cs b/windows/TweetDuck/Browser/Base/CefRequestAdapter.cs index 2722d6b7..ff0361b0 100644 --- a/windows/TweetDuck/Browser/Base/CefRequestAdapter.cs +++ b/windows/TweetDuck/Browser/Base/CefRequestAdapter.cs @@ -20,6 +20,10 @@ public void SetUrl(IRequest request, string url) { request.Url = url; } + public void SetMethod(IRequest request, string method) { + request.Method = method; + } + public bool IsTransitionForwardBack(IRequest request) { return request.TransitionType.HasFlag(TransitionType.ForwardBack); } @@ -42,5 +46,13 @@ public ResourceType GetResourceType(IRequest request) { public void SetHeader(IRequest request, string header, string value) { request.SetHeaderByName(header, value, overwrite: true); } + + public void SetReferrer(IRequest request, string referrer) { + request.SetReferrer(referrer, ReferrerPolicy.Default); + } + + public void SetAllowStoredCredentials(IRequest request) { + request.Flags |= UrlRequestFlags.AllowStoredCredentials; + } } } diff --git a/windows/TweetDuck/Browser/Base/CefResourceRequestHandler.cs b/windows/TweetDuck/Browser/Base/CefResourceRequestHandler.cs index 4925810b..6ac55e5b 100644 --- a/windows/TweetDuck/Browser/Base/CefResourceRequestHandler.cs +++ b/windows/TweetDuck/Browser/Base/CefResourceRequestHandler.cs @@ -21,8 +21,7 @@ protected override IResourceHandler GetResourceHandler(IWebBrowser browserContro } protected override IResponseFilter GetResourceResponseFilter(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response) { - var filter = logic.GetResourceResponseFilter(request, response); - return filter == null ? null : new CefResponseFilter(filter); + return CefResponseFilter.Create(logic.GetResourceResponseFilter(request, response)); } protected override void OnResourceLoadComplete(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response, UrlRequestStatus status, long receivedContentLength) { diff --git a/windows/TweetDuck/Browser/Base/CefResponseFilter.cs b/windows/TweetDuck/Browser/Base/CefResponseFilter.cs index ad2fb042..466359b4 100644 --- a/windows/TweetDuck/Browser/Base/CefResponseFilter.cs +++ b/windows/TweetDuck/Browser/Base/CefResponseFilter.cs @@ -5,9 +5,13 @@ namespace TweetDuck.Browser.Base { sealed class CefResponseFilter : IResponseFilter { + public static CefResponseFilter Create(ResponseFilterLogic logic) { + return logic == null ? null : new CefResponseFilter(logic); + } + private readonly ResponseFilterLogic logic; - public CefResponseFilter(ResponseFilterLogic logic) { + private CefResponseFilter(ResponseFilterLogic logic) { this.logic = logic; } diff --git a/windows/TweetDuck/Browser/Handling/ContextMenuBase.cs b/windows/TweetDuck/Browser/Handling/ContextMenuBase.cs index 99cc7edd..85fb4ed7 100644 --- a/windows/TweetDuck/Browser/Handling/ContextMenuBase.cs +++ b/windows/TweetDuck/Browser/Handling/ContextMenuBase.cs @@ -1,76 +1,27 @@ -using System.Collections.Generic; -using System.Drawing; +using System.Drawing; using CefSharp; using TweetDuck.Browser.Base; using TweetDuck.Configuration; using TweetDuck.Utils; -using TweetLib.Browser.CEF.Data; using TweetLib.Browser.Contexts; +using TweetLib.Core.Features.TweetDeck; +using TweetLib.Core.Features.Twitter; +using IContextMenuHandler = TweetLib.Browser.Interfaces.IContextMenuHandler; namespace TweetDuck.Browser.Handling { - class ContextMenuBase : IContextMenuHandler { + class ContextMenuBase : CefContextMenuHandler { private const CefMenuCommand MenuOpenDevTools = (CefMenuCommand) 26500; - private static readonly HashSet<CefMenuCommand> AllowedCefCommands = new HashSet<CefMenuCommand> { - CefMenuCommand.NotFound, - CefMenuCommand.Undo, - CefMenuCommand.Redo, - CefMenuCommand.Cut, - CefMenuCommand.Copy, - CefMenuCommand.Paste, - CefMenuCommand.Delete, - CefMenuCommand.SelectAll, - CefMenuCommand.SpellCheckSuggestion0, - CefMenuCommand.SpellCheckSuggestion1, - CefMenuCommand.SpellCheckSuggestion2, - CefMenuCommand.SpellCheckSuggestion3, - CefMenuCommand.SpellCheckSuggestion4, - CefMenuCommand.SpellCheckNoSuggestions, - CefMenuCommand.AddToDictionary - }; - protected static UserConfig Config => Program.Config.User; - private readonly TweetLib.Browser.Interfaces.IContextMenuHandler handler; - private readonly ContextMenuActionRegistry actionRegistry; + public ContextMenuBase(IContextMenuHandler handler) : base(handler) {} - public ContextMenuBase(TweetLib.Browser.Interfaces.IContextMenuHandler handler) { - this.handler = handler; - this.actionRegistry = new ContextMenuActionRegistry(); + protected override Context CreateContext(IContextMenuParams parameters) { + return CreateContext(parameters, null, Config.TwitterImageQuality); } - private sealed class ContextMenuActionRegistry : ContextMenuActionRegistry<CefMenuCommand> { - protected override CefMenuCommand NextId(int n) { - return CefMenuCommand.UserFirst + 500 + n; - } - } - - protected virtual Context CreateContext(IContextMenuParams parameters) { - return CefContextMenuModel.CreateContext(parameters, null, Config.TwitterImageQuality); - } - - public virtual void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) { - for (int i = model.Count - 1; i >= 0; i--) { - CefMenuCommand command = model.GetCommandIdAt(i); - - if (!AllowedCefCommands.Contains(command) && !(command >= CefMenuCommand.CustomFirst && command <= CefMenuCommand.CustomLast)) { - model.RemoveAt(i); - } - } - - for (int i = model.Count - 2; i >= 0; i--) { - if (model.GetTypeAt(i) == MenuItemType.Separator && model.GetTypeAt(i + 1) == MenuItemType.Separator) { - model.RemoveAt(i); - } - } - - if (model.Count > 0 && model.GetTypeAt(0) == MenuItemType.Separator) { - model.RemoveAt(0); - } - - AddSeparator(model); - handler?.Show(new CefContextMenuModel(model, actionRegistry), CreateContext(parameters)); - RemoveSeparatorIfLast(model); + public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) { + base.OnBeforeContextMenu(browserControl, browser, frame, parameters, model); AddLastContextMenuItems(model); } @@ -78,8 +29,8 @@ protected virtual void AddLastContextMenuItems(IMenuModel model) { AddDebugMenuItems(model); } - public virtual bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags) { - if (actionRegistry.Execute(commandId)) { + public override bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags) { + if (base.OnContextMenuCommand(browserControl, browser, frame, parameters, commandId, eventFlags)) { return true; } @@ -91,14 +42,6 @@ public virtual bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser br return false; } - public virtual void OnContextMenuDismissed(IWebBrowser browserControl, IBrowser browser, IFrame frame) { - actionRegistry.Clear(); - } - - public virtual bool RunContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model, IRunContextMenuCallback callback) { - return false; - } - protected static void AddDebugMenuItems(IMenuModel model) { if (Config.DevToolsInContextMenu) { AddSeparator(model); @@ -107,15 +50,54 @@ protected static void AddDebugMenuItems(IMenuModel model) { } protected static void AddSeparator(IMenuModel model) { - if (model.Count > 0 && model.GetTypeAt(model.Count - 1) != MenuItemType.Separator) { // do not add separators if there is nothing to separate + if (model.Count > 0 && model.GetTypeAt(model.Count - 1) != MenuItemType.Separator) { model.AddSeparator(); } } - private static void RemoveSeparatorIfLast(IMenuModel model) { - if (model.Count > 0 && model.GetTypeAt(model.Count - 1) == MenuItemType.Separator) { - model.RemoveAt(model.Count - 1); + protected static Context CreateContext(IContextMenuParams parameters, TweetDeckExtraContext extraContext, ImageQuality imageQuality) { + var context = new Context(); + var flags = parameters.TypeFlags; + + var tweet = extraContext?.Tweet; + if (tweet != null && !flags.HasFlag(ContextMenuType.Editable)) { + context.Tweet = tweet; } + + context.Link = GetLink(parameters, extraContext); + context.Media = GetMedia(parameters, extraContext, imageQuality); + + if (flags.HasFlag(ContextMenuType.Selection)) { + context.Selection = new Selection(parameters.SelectionText, flags.HasFlag(ContextMenuType.Editable)); + } + + return context; + } + + private static Link? GetLink(IContextMenuParams parameters, TweetDeckExtraContext extraContext) { + var link = extraContext?.Link; + if (link != null) { + return link; + } + + if (parameters.TypeFlags.HasFlag(ContextMenuType.Link) && extraContext?.Media == null) { + return new Link(parameters.LinkUrl, parameters.UnfilteredLinkUrl); + } + + return null; + } + + private static Media? GetMedia(IContextMenuParams parameters, TweetDeckExtraContext extraContext, ImageQuality imageQuality) { + var media = extraContext?.Media; + if (media != null) { + return media; + } + + if (parameters.TypeFlags.HasFlag(ContextMenuType.Media) && parameters.HasImageContents) { + return new Media(Media.Type.Image, TwitterUrls.GetMediaLink(parameters.SourceUrl, imageQuality)); + } + + return null; } } } diff --git a/windows/TweetDuck/Browser/Handling/ContextMenuBrowser.cs b/windows/TweetDuck/Browser/Handling/ContextMenuBrowser.cs index 5b59d4c6..50c420cf 100644 --- a/windows/TweetDuck/Browser/Handling/ContextMenuBrowser.cs +++ b/windows/TweetDuck/Browser/Handling/ContextMenuBrowser.cs @@ -1,6 +1,5 @@ using System.Windows.Forms; using CefSharp; -using TweetDuck.Browser.Base; using TweetDuck.Controls; using TweetLib.Browser.Contexts; using TweetLib.Core.Features.TweetDeck; @@ -31,7 +30,7 @@ public ContextMenuBrowser(FormBrowser form, IContextMenuHandler handler, TweetDe } protected override Context CreateContext(IContextMenuParams parameters) { - return CefContextMenuModel.CreateContext(parameters, extraContext, Config.TwitterImageQuality); + return CreateContext(parameters, extraContext, Config.TwitterImageQuality); } public override void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model) { diff --git a/windows/TweetDuck/TweetDuck.csproj b/windows/TweetDuck/TweetDuck.csproj index db4db178..6a4a0cab 100644 --- a/windows/TweetDuck/TweetDuck.csproj +++ b/windows/TweetDuck/TweetDuck.csproj @@ -95,15 +95,17 @@ <Compile Include="Application\FileDialogs.cs" /> <Compile Include="Application\MessageDialogs.cs" /> <Compile Include="Application\SystemHandler.cs" /> + <Compile Include="Browser\Base\CefAdapter.cs" /> <Compile Include="Browser\Base\CefBrowserAdapter.cs" /> <Compile Include="Browser\Base\CefBrowserComponent.cs" /> - <Compile Include="Browser\Base\CefContextMenuModel.cs" /> + <Compile Include="Browser\Base\CefContextMenuHandler.cs" /> <Compile Include="Browser\Base\CefDownloadRequestClient.cs" /> <Compile Include="Browser\Base\CefDragDataAdapter.cs" /> <Compile Include="Browser\Base\CefDragHandler.cs" /> <Compile Include="Browser\Base\CefErrorCodeAdapter.cs" /> <Compile Include="Browser\Base\CefFrameAdapter.cs" /> <Compile Include="Browser\Base\CefLifeSpanHandler.cs" /> + <Compile Include="Browser\Base\CefMenuModelAdapter.cs" /> <Compile Include="Browser\Base\CefRequestAdapter.cs" /> <Compile Include="Browser\Base\CefRequestHandler.cs" /> <Compile Include="Browser\Base\CefResourceHandlerFactory.cs" />