csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
505 lines
18 KiB
505 lines
18 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using Avalonia.Controls.Platform.Surfaces;
|
|
using Avalonia.Input;
|
|
using Avalonia.Input.Raw;
|
|
using Avalonia.Platform;
|
|
using Avalonia.Rendering;
|
|
using Avalonia.Threading;
|
|
using MonoMac.AppKit;
|
|
using MonoMac.CoreGraphics;
|
|
using MonoMac.Foundation;
|
|
using MonoMac.ObjCRuntime;
|
|
|
|
namespace Avalonia.MonoMac
|
|
{
|
|
abstract class TopLevelImpl : ITopLevelImpl, IFramebufferPlatformSurface
|
|
{
|
|
public TopLevelView View { get; }
|
|
private readonly IMouseDevice _mouse = AvaloniaLocator.Current.GetService<IMouseDevice>();
|
|
private readonly IDragDropDevice _dragDevice = AvaloniaLocator.Current.GetService<IDragDropDevice>();
|
|
protected TopLevelImpl()
|
|
{
|
|
View = new TopLevelView(this);
|
|
}
|
|
|
|
protected virtual void OnInput(RawInputEventArgs args)
|
|
{
|
|
Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
|
|
Input?.Invoke(args);
|
|
}
|
|
|
|
[Adopts("NSTextInputClient")]
|
|
public class TopLevelView : NSView
|
|
{
|
|
TopLevelImpl _tl;
|
|
bool _isLeftPressed, _isRightPressed, _isMiddlePressed;
|
|
private readonly IMouseDevice _mouse;
|
|
private readonly IKeyboardDevice _keyboard;
|
|
private NSTrackingArea _area;
|
|
private NSCursor _cursor;
|
|
private bool _nonUiRedrawQueued;
|
|
private bool _isMouseOver;
|
|
|
|
public CGSize PixelSize { get; set; }
|
|
|
|
public CGSize LogicalSize { get; set; }
|
|
|
|
private SavedImage _backBuffer;
|
|
public object SyncRoot { get; } = new object();
|
|
|
|
public TopLevelView(TopLevelImpl tl)
|
|
{
|
|
_tl = tl;
|
|
_mouse = AvaloniaLocator.Current.GetService<IMouseDevice>();
|
|
_keyboard = AvaloniaLocator.Current.GetService<IKeyboardDevice>();
|
|
|
|
RegisterForDraggedTypes(new string[] {
|
|
"public.data" // register for any kind of data.
|
|
});
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
_backBuffer?.Dispose();
|
|
_backBuffer = null;
|
|
}
|
|
base.Dispose(disposing);
|
|
}
|
|
|
|
public override bool ConformsToProtocol(IntPtr protocol)
|
|
{
|
|
var rv = base.ConformsToProtocol(protocol);
|
|
return rv;
|
|
}
|
|
|
|
public override bool IsOpaque => false;
|
|
|
|
public override void DrawRect(CGRect dirtyRect)
|
|
{
|
|
lock (SyncRoot)
|
|
_nonUiRedrawQueued = false;
|
|
Dispatcher.UIThread.RunJobs(DispatcherPriority.Render);
|
|
lock (SyncRoot)
|
|
{
|
|
if (_backBuffer != null)
|
|
{
|
|
using (var context = NSGraphicsContext.CurrentContext.GraphicsPort)
|
|
{
|
|
context.SetFillColor(255, 255, 255, 255);
|
|
context.FillRect(new CGRect(default(CGPoint), LogicalSize));
|
|
context.TranslateCTM(0, LogicalSize.Height - _backBuffer.LogicalSize.Height);
|
|
context.DrawImage(new CGRect(default(CGPoint), _backBuffer.LogicalSize), _backBuffer.Image);
|
|
context.Flush();
|
|
NSGraphicsContext.CurrentContext.FlushGraphics();
|
|
}
|
|
}
|
|
}
|
|
_tl.Paint?.Invoke(dirtyRect.ToAvaloniaRect());
|
|
}
|
|
|
|
public void SetBackBufferImage(SavedImage image)
|
|
{
|
|
lock (SyncRoot)
|
|
{
|
|
_backBuffer?.Dispose();
|
|
_backBuffer = image;
|
|
if (image == null)
|
|
return;
|
|
|
|
if (_nonUiRedrawQueued)
|
|
return;
|
|
_nonUiRedrawQueued = true;
|
|
Dispatcher.UIThread.Post(
|
|
() =>
|
|
{
|
|
lock (SyncRoot)
|
|
{
|
|
if (!_nonUiRedrawQueued)
|
|
return;
|
|
_nonUiRedrawQueued = false;
|
|
}
|
|
SetNeedsDisplayInRect(Frame);
|
|
Display();
|
|
}, DispatcherPriority.Render);
|
|
|
|
}
|
|
}
|
|
|
|
[Export("viewDidChangeBackingProperties:")]
|
|
public void ViewDidChangeBackingProperties()
|
|
{
|
|
_tl?.ScalingChanged?.Invoke(_tl.Scaling);
|
|
}
|
|
|
|
void UpdateCursor()
|
|
{
|
|
ResetCursorRects();
|
|
if (_cursor != null)
|
|
{
|
|
AddCursorRect(Frame, _cursor);
|
|
if (_isMouseOver)
|
|
_cursor.Set();
|
|
}
|
|
}
|
|
|
|
static readonly NSCursor ArrowCursor = NSCursor.ArrowCursor;
|
|
|
|
public void SetCursor(NSCursor cursor)
|
|
{
|
|
_cursor = cursor ?? ArrowCursor;
|
|
UpdateCursor();
|
|
}
|
|
|
|
private NSDragOperation SendRawDragEvent(NSDraggingInfo sender, RawDragEventType type)
|
|
{
|
|
Action<RawInputEventArgs> input = _tl.Input;
|
|
IDragDropDevice dragDevice = _tl._dragDevice;
|
|
IInputRoot root = _tl?.InputRoot;
|
|
if (root == null || dragDevice == null || input == null)
|
|
return NSDragOperation.None;
|
|
|
|
var dragOp = DraggingInfo.ConvertDragOperation(sender.DraggingSourceOperationMask);
|
|
DraggingInfo info = new DraggingInfo(sender);
|
|
|
|
var pt = TranslateLocalPoint(info.Location);
|
|
var args = new RawDragEvent(dragDevice, type, root, pt, info, dragOp, GetModifiers(NSEvent.CurrentModifierFlags));
|
|
input(args);
|
|
return DraggingInfo.ConvertDragOperation(args.Effects);
|
|
}
|
|
|
|
public override NSDragOperation DraggingEntered(NSDraggingInfo sender)
|
|
{
|
|
return SendRawDragEvent(sender, RawDragEventType.DragEnter);
|
|
}
|
|
|
|
public override NSDragOperation DraggingUpdated(NSDraggingInfo sender)
|
|
{
|
|
return SendRawDragEvent(sender, RawDragEventType.DragOver);
|
|
}
|
|
|
|
public override void DraggingExited(NSDraggingInfo sender)
|
|
{
|
|
SendRawDragEvent(sender, RawDragEventType.DragLeave);
|
|
}
|
|
|
|
public override bool PrepareForDragOperation(NSDraggingInfo sender)
|
|
{
|
|
return SendRawDragEvent(sender, RawDragEventType.DragOver) != NSDragOperation.None;
|
|
}
|
|
|
|
public override bool PerformDragOperation(NSDraggingInfo sender)
|
|
{
|
|
return SendRawDragEvent(sender, RawDragEventType.Drop) != NSDragOperation.None;
|
|
}
|
|
|
|
|
|
public override void SetFrameSize(CGSize newSize)
|
|
{
|
|
lock (SyncRoot)
|
|
{
|
|
base.SetFrameSize(newSize);
|
|
LogicalSize = Frame.Size;
|
|
PixelSize = ConvertSizeToBacking(LogicalSize);
|
|
}
|
|
|
|
if (_area != null)
|
|
{
|
|
RemoveTrackingArea(_area);
|
|
_area.Dispose();
|
|
}
|
|
_area = new NSTrackingArea(new CGRect(default(CGPoint), newSize),
|
|
NSTrackingAreaOptions.ActiveAlways |
|
|
NSTrackingAreaOptions.MouseMoved |
|
|
NSTrackingAreaOptions.EnabledDuringMouseDrag, this, null);
|
|
AddTrackingArea(_area);
|
|
UpdateCursor();
|
|
_tl?.Resized?.Invoke(_tl.ClientSize);
|
|
Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout);
|
|
}
|
|
|
|
InputModifiers GetModifiers(NSEventModifierMask mod)
|
|
{
|
|
var rv = new InputModifiers();
|
|
if (mod.HasFlag(NSEventModifierMask.ControlKeyMask))
|
|
rv |= InputModifiers.Control;
|
|
if (mod.HasFlag(NSEventModifierMask.ShiftKeyMask))
|
|
rv |= InputModifiers.Shift;
|
|
if (mod.HasFlag(NSEventModifierMask.AlternateKeyMask))
|
|
rv |= InputModifiers.Alt;
|
|
if (mod.HasFlag(NSEventModifierMask.CommandKeyMask))
|
|
rv |= InputModifiers.Windows;
|
|
|
|
if (_isLeftPressed)
|
|
rv |= InputModifiers.LeftMouseButton;
|
|
if (_isMiddlePressed)
|
|
rv |= InputModifiers.MiddleMouseButton;
|
|
if (_isRightPressed)
|
|
rv |= InputModifiers.RightMouseButton;
|
|
return rv;
|
|
}
|
|
|
|
public Point TranslateLocalPoint(Point pt) => pt.WithY(Bounds.Height - pt.Y);
|
|
|
|
Vector GetDelta(NSEvent ev)
|
|
{
|
|
var rv = new Vector(ev.ScrollingDeltaX, ev.ScrollingDeltaY);
|
|
//TODO: Verify if handling of HasPreciseScrollingDeltas
|
|
// is required (touchpad or magic-mouse is needed)
|
|
return rv;
|
|
}
|
|
|
|
uint GetTimeStamp(NSEvent ev) => (uint) (ev.Timestamp * 1000);
|
|
|
|
void MouseEvent(NSEvent ev, RawMouseEventType type)
|
|
{
|
|
BecomeFirstResponder();
|
|
var loc = TranslateLocalPoint(ConvertPointToView(ev.LocationInWindow, this).ToAvaloniaPoint());
|
|
var ts = GetTimeStamp(ev);
|
|
var mod = GetModifiers(ev.ModifierFlags);
|
|
if (type == RawMouseEventType.Wheel)
|
|
{
|
|
var delta = GetDelta(ev);
|
|
// ReSharper disable CompareOfFloatsByEqualityOperator
|
|
if (delta.X == 0 && delta.Y == 0)
|
|
return;
|
|
// ReSharper restore CompareOfFloatsByEqualityOperator
|
|
_tl.OnInput(new RawMouseWheelEventArgs(_mouse, ts, _tl.InputRoot, loc,
|
|
delta, mod));
|
|
}
|
|
else
|
|
_tl.OnInput(new RawMouseEventArgs(_mouse, ts, _tl.InputRoot, type, loc, mod));
|
|
}
|
|
|
|
public override void MouseMoved(NSEvent theEvent)
|
|
{
|
|
MouseEvent(theEvent, RawMouseEventType.Move);
|
|
base.MouseMoved(theEvent);
|
|
}
|
|
|
|
public override void MouseDragged(NSEvent theEvent)
|
|
{
|
|
MouseEvent(theEvent, RawMouseEventType.Move);
|
|
base.MouseDragged(theEvent);
|
|
}
|
|
|
|
public override void OtherMouseDragged(NSEvent theEvent)
|
|
{
|
|
MouseEvent(theEvent, RawMouseEventType.Move);
|
|
base.OtherMouseDragged(theEvent);
|
|
}
|
|
|
|
public override void RightMouseDragged(NSEvent theEvent)
|
|
{
|
|
MouseEvent(theEvent, RawMouseEventType.Move);
|
|
base.RightMouseDragged(theEvent);
|
|
}
|
|
|
|
public NSEvent LastMouseDownEvent { get; private set; }
|
|
|
|
public override void MouseDown(NSEvent theEvent)
|
|
{
|
|
_isLeftPressed = true;
|
|
LastMouseDownEvent = theEvent;
|
|
MouseEvent(theEvent, RawMouseEventType.LeftButtonDown);
|
|
LastMouseDownEvent = null;
|
|
base.MouseDown(theEvent);
|
|
}
|
|
|
|
public override void RightMouseDown(NSEvent theEvent)
|
|
{
|
|
_isRightPressed = true;
|
|
MouseEvent(theEvent, RawMouseEventType.RightButtonDown);
|
|
base.RightMouseDown(theEvent);
|
|
}
|
|
|
|
public override void OtherMouseDown(NSEvent theEvent)
|
|
{
|
|
_isMiddlePressed = true;
|
|
MouseEvent(theEvent, RawMouseEventType.MiddleButtonDown);
|
|
base.OtherMouseDown(theEvent);
|
|
}
|
|
|
|
public override void MouseUp(NSEvent theEvent)
|
|
{
|
|
_isLeftPressed = false;
|
|
MouseEvent(theEvent, RawMouseEventType.LeftButtonUp);
|
|
base.MouseUp(theEvent);
|
|
}
|
|
|
|
public override void RightMouseUp(NSEvent theEvent)
|
|
{
|
|
_isRightPressed = false;
|
|
MouseEvent(theEvent, RawMouseEventType.RightButtonUp);
|
|
base.RightMouseUp(theEvent);
|
|
}
|
|
|
|
public override void OtherMouseUp(NSEvent theEvent)
|
|
{
|
|
_isMiddlePressed = false;
|
|
MouseEvent(theEvent, RawMouseEventType.MiddleButtonUp);
|
|
base.OtherMouseUp(theEvent);
|
|
}
|
|
|
|
public override void ScrollWheel(NSEvent theEvent)
|
|
{
|
|
MouseEvent(theEvent, RawMouseEventType.Wheel);
|
|
base.ScrollWheel(theEvent);
|
|
}
|
|
|
|
public override void MouseExited(NSEvent theEvent)
|
|
{
|
|
_isMouseOver = false;
|
|
MouseEvent(theEvent, RawMouseEventType.LeaveWindow);
|
|
base.MouseExited(theEvent);
|
|
}
|
|
|
|
public override void MouseEntered(NSEvent theEvent)
|
|
{
|
|
_isMouseOver = true;
|
|
base.MouseEntered(theEvent);
|
|
}
|
|
|
|
void KeyboardEvent(RawKeyEventType type, NSEvent ev)
|
|
{
|
|
var code = KeyTransform.TransformKeyCode(ev.KeyCode);
|
|
if (!code.HasValue)
|
|
return;
|
|
_tl.OnInput(new RawKeyEventArgs(_keyboard, GetTimeStamp(ev),
|
|
type, code.Value, GetModifiers(ev.ModifierFlags)));
|
|
}
|
|
|
|
public override void KeyDown(NSEvent theEvent)
|
|
{
|
|
KeyboardEvent(RawKeyEventType.KeyDown, theEvent);
|
|
InputContext.HandleEvent(theEvent);
|
|
base.KeyDown(theEvent);
|
|
}
|
|
|
|
public override void KeyUp(NSEvent theEvent)
|
|
{
|
|
KeyboardEvent(RawKeyEventType.KeyUp, theEvent);
|
|
base.KeyUp(theEvent);
|
|
}
|
|
|
|
|
|
|
|
#region NSTextInputClient
|
|
|
|
public override bool AcceptsFirstResponder() => true;
|
|
|
|
public bool HasMarkedText
|
|
{
|
|
[Export("hasMarkedText")] get => false;
|
|
}
|
|
|
|
public NSRange MarkedRange
|
|
{
|
|
[Export("markedRange")] get => new NSRange(NSRange.NotFound, 0);
|
|
}
|
|
|
|
public NSRange SelectedRange
|
|
{
|
|
[Export("selectedRange")] get => new NSRange(NSRange.NotFound, 0);
|
|
}
|
|
|
|
[Export("setMarkedText:selectedRange:replacementRange:")]
|
|
public void SetMarkedText(NSString str, NSRange a1, NSRange a2)
|
|
{
|
|
|
|
}
|
|
|
|
[Export("unmarkText")]
|
|
public void UnmarkText()
|
|
{
|
|
|
|
}
|
|
|
|
public NSArray ValidAttributesForMarkedText
|
|
{
|
|
[Export("validAttributesForMarkedText")] get => new NSArray();
|
|
}
|
|
|
|
[Export("attributedSubstringForProposedRange:actualRange:")]
|
|
public NSAttributedString AttributedSubstringForProposedRange(NSRange range, IntPtr wat)
|
|
{
|
|
return new NSAttributedString("");
|
|
}
|
|
|
|
[Export("insertText:replacementRange:")]
|
|
public void InsertText(NSString str, NSRange range)
|
|
{
|
|
//TODO: timestamp
|
|
_tl.OnInput(new RawTextInputEventArgs(_keyboard, 0, str.ToString()));
|
|
}
|
|
|
|
[Export("characterIndexForPoint:")]
|
|
public uint CharacterIndexForPoint(CGPoint pt)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
[Export("firstRectForCharacterRange:actualRange:")]
|
|
public CGRect FirstRectForCharacterRange(NSRange range, IntPtr wat)
|
|
{
|
|
return new CGRect();
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
public IInputRoot InputRoot { get; private set; }
|
|
|
|
public abstract Size ClientSize { get; }
|
|
|
|
public double Scaling
|
|
{
|
|
get
|
|
{
|
|
if (View.Window == null)
|
|
return 1;
|
|
return View.Window.BackingScaleFactor;
|
|
}
|
|
}
|
|
|
|
public IEnumerable<object> Surfaces => new[] {this};
|
|
public IMouseDevice MouseDevice => _mouse;
|
|
|
|
public Action<RawInputEventArgs> Input { get; set; }
|
|
public Action<Rect> Paint { get; set; }
|
|
public Action<Size> Resized { get; set; }
|
|
public Action<double> ScalingChanged { get; set; }
|
|
public Action Closed { get; set; }
|
|
|
|
|
|
public virtual void Dispose()
|
|
{
|
|
Closed?.Invoke();
|
|
Closed = null;
|
|
View.Dispose();
|
|
}
|
|
|
|
public IRenderer CreateRenderer(IRenderRoot root) =>
|
|
MonoMacPlatform.UseDeferredRendering
|
|
? new DeferredRenderer(root, AvaloniaLocator.Current.GetService<IRenderLoop>())
|
|
: (IRenderer) new ImmediateRenderer(root);
|
|
|
|
public void Invalidate(Rect rect)
|
|
{
|
|
if (!MonoMacPlatform.UseDeferredRendering)
|
|
View.SetNeedsDisplayInRect(View.Frame);
|
|
}
|
|
|
|
public abstract Point PointToClient(Point point);
|
|
|
|
public abstract Point PointToScreen(Point point);
|
|
|
|
public void SetCursor(IPlatformHandle cursor) => View.SetCursor((cursor as Cursor)?.Native);
|
|
|
|
public void SetInputRoot(IInputRoot inputRoot) => InputRoot = inputRoot;
|
|
|
|
public ILockedFramebuffer Lock() => new EmulatedFramebuffer(View);
|
|
}
|
|
}
|
|
|