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:
parent
0a9c84feec
commit
3aace0b399
lib/TweetLib.Browser.CEF
Component
Data
Interfaces
Logic
windows/TweetDuck
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
33
lib/TweetLib.Browser.CEF/Data/DownloadCallbacks.cs
Normal file
33
lib/TweetLib.Browser.CEF/Data/DownloadCallbacks.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
7
lib/TweetLib.Browser.CEF/Interfaces/ICefAdapter.cs
Normal file
7
lib/TweetLib.Browser.CEF/Interfaces/ICefAdapter.cs
Normal file
@ -0,0 +1,7 @@
|
||||
using System;
|
||||
|
||||
namespace TweetLib.Browser.CEF.Interfaces {
|
||||
public interface ICefAdapter {
|
||||
void RunOnUiThread(Action action);
|
||||
}
|
||||
}
|
16
lib/TweetLib.Browser.CEF/Interfaces/IMenuModelAdapter.cs
Normal file
16
lib/TweetLib.Browser.CEF/Interfaces/IMenuModelAdapter.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
125
lib/TweetLib.Browser.CEF/Logic/ContextMenuLogic.cs
Normal file
125
lib/TweetLib.Browser.CEF/Logic/ContextMenuLogic.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
15
windows/TweetDuck/Browser/Base/CefAdapter.cs
Normal file
15
windows/TweetDuck/Browser/Base/CefAdapter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
31
windows/TweetDuck/Browser/Base/CefContextMenuHandler.cs
Normal file
31
windows/TweetDuck/Browser/Base/CefContextMenuHandler.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
42
windows/TweetDuck/Browser/Base/CefMenuModelAdapter.cs
Normal file
42
windows/TweetDuck/Browser/Base/CefMenuModelAdapter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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" />
|
||||
|
Loading…
Reference in New Issue
Block a user