1
0
mirror of https://github.com/chylex/TweetDuck.git synced 2025-04-23 21:15:49 +02:00

Work on abstracting CEF conventions and logic into a separate library

This commit is contained in:
chylex 2022-02-02 20:28:23 +01:00
parent 0a9c84feec
commit 3aace0b399
Signed by: chylex
GPG Key ID: 4DE42C8F19A80548
23 changed files with 422 additions and 222 deletions

View File

@ -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);
}
});
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,7 @@
using System;
namespace TweetLib.Browser.CEF.Interfaces {
public interface ICefAdapter {
void RunOnUiThread(Action action);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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));
}
}
}

View File

@ -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);
}
});
}
}
}

View File

@ -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);
};

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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) {

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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) {

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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) {

View File

@ -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" />