diff --git a/src/Gtk/Perspex.Gtk/CursorFactory.cs b/src/Gtk/Perspex.Gtk/CursorFactory.cs new file mode 100644 index 0000000000..c3e90f578c --- /dev/null +++ b/src/Gtk/Perspex.Gtk/CursorFactory.cs @@ -0,0 +1,77 @@ +namespace Perspex.Gtk +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using Gdk; + using Gtk = global::Gtk; + using Perspex.Input; + using Perspex.Platform; + class CursorFactory : IStandardCursorFactory + { + public static CursorFactory Instance { get; } = new CursorFactory(); + + private CursorFactory() + { + } + + private static readonly Dictionary CursorTypeMapping = new Dictionary + + { + { StandardCursorType.AppStarting, CursorType.Watch }, + { StandardCursorType.Arrow, CursorType.LeftPtr }, + { StandardCursorType.Cross, CursorType.Cross }, + { StandardCursorType.Hand, CursorType.Hand1 }, + { StandardCursorType.Ibeam, CursorType.Xterm }, + { StandardCursorType.No, Gtk.Stock.Cancel}, + { StandardCursorType.SizeAll, CursorType.Sizing }, + //{ StandardCursorType.SizeNorthEastSouthWest, 32643 }, + { StandardCursorType.SizeNorthSouth, CursorType.SbVDoubleArrow}, + //{ StandardCursorType.SizeNorthWestSouthEast, 32642 }, + { StandardCursorType.SizeWestEast, CursorType.SbHDoubleArrow }, + { StandardCursorType.UpArrow, CursorType.BasedArrowUp }, + { StandardCursorType.Wait, CursorType.Watch }, + { StandardCursorType.Help, Gtk.Stock.Help } + }; + + private static readonly Dictionary Cache = + new Dictionary(); + + Gdk.Cursor GetCursor(object desc) + { + Gdk.Cursor rv; + var name = desc as string; + if (name != null) + { + var theme = Gtk.IconTheme.Default; + var icon = theme.LoadIcon(name, 32, default(Gtk.IconLookupFlags)); + rv = icon == null ? new Gdk.Cursor(CursorType.XCursor) : new Gdk.Cursor(Gdk.Display.Default, icon, 0, 0); + } + else + { + rv = new Gdk.Cursor((CursorType) desc); + } + + rv.Owned = false; + return rv; + + } + + public IPlatformHandle GetCursor(StandardCursorType cursorType) + { + IPlatformHandle rv; + if (!Cache.TryGetValue(cursorType, out rv)) + { + Cache[cursorType] = + rv = + new PlatformHandle( + GetCursor(CursorTypeMapping[cursorType]).Handle, + "GTKCURSOR"); + } + + return rv; + } + } +} diff --git a/src/Gtk/Perspex.Gtk/GtkPlatform.cs b/src/Gtk/Perspex.Gtk/GtkPlatform.cs index 8bfecb537b..8d51b7bd5e 100644 --- a/src/Gtk/Perspex.Gtk/GtkPlatform.cs +++ b/src/Gtk/Perspex.Gtk/GtkPlatform.cs @@ -43,6 +43,7 @@ namespace Perspex.Gtk locator.Register(() => new WindowImpl(), typeof(IWindowImpl)); locator.Register(() => new PopupImpl(), typeof(IPopupImpl)); locator.Register(() => new ClipboardImpl(), typeof (IClipboard)); + locator.Register(() => CursorFactory.Instance, typeof(IStandardCursorFactory)); locator.Register(() => GtkKeyboardDevice.Instance, typeof(IKeyboardDevice)); locator.Register(() => instance, typeof(IPlatformSettings)); locator.Register(() => instance, typeof(IPlatformThreadingInterface)); diff --git a/src/Gtk/Perspex.Gtk/Perspex.Gtk.csproj b/src/Gtk/Perspex.Gtk/Perspex.Gtk.csproj index b1c83bfbbb..563d87b2a3 100644 --- a/src/Gtk/Perspex.Gtk/Perspex.Gtk.csproj +++ b/src/Gtk/Perspex.Gtk/Perspex.Gtk.csproj @@ -60,6 +60,7 @@ + diff --git a/src/Gtk/Perspex.Gtk/WindowImpl.cs b/src/Gtk/Perspex.Gtk/WindowImpl.cs index 91dfe3eef2..39fc0a40ca 100644 --- a/src/Gtk/Perspex.Gtk/WindowImpl.cs +++ b/src/Gtk/Perspex.Gtk/WindowImpl.cs @@ -29,6 +29,8 @@ namespace Perspex.Gtk private uint lastKeyEventTimestamp; + private static readonly Gdk.Cursor DefaultCursor = new Gdk.Cursor(CursorType.LeftPtr); + public WindowImpl() : base(Gtk.WindowType.Toplevel) { @@ -105,6 +107,12 @@ namespace Perspex.Gtk this.Title = title; } + + public void SetCursor(IPlatformHandle cursor) + { + GdkWindow.Cursor = cursor != null ? new Gdk.Cursor(cursor.Handle) : DefaultCursor; + } + public IDisposable ShowDialog() { this.Modal = true; diff --git a/src/Perspex.Controls/Platform/ITopLevelImpl.cs b/src/Perspex.Controls/Platform/ITopLevelImpl.cs index 2802dad621..3344bd47a2 100644 --- a/src/Perspex.Controls/Platform/ITopLevelImpl.cs +++ b/src/Perspex.Controls/Platform/ITopLevelImpl.cs @@ -36,5 +36,11 @@ namespace Perspex.Platform void SetOwner(TopLevel owner); Point PointToScreen(Point point); + + /// + /// Sets the cursor associated with the window. + /// + /// The cursor. Use null for default cursor + void SetCursor(IPlatformHandle cursor); } } diff --git a/src/Perspex.Controls/TextBox.cs b/src/Perspex.Controls/TextBox.cs index f468b07702..55ba845835 100644 --- a/src/Perspex.Controls/TextBox.cs +++ b/src/Perspex.Controls/TextBox.cs @@ -115,6 +115,7 @@ namespace Perspex.Controls protected override void OnTemplateApplied() { this.presenter = this.GetTemplateChild("textPresenter"); + this.presenter.Cursor = new Cursor(StandardCursorType.Ibeam); } protected override void OnGotFocus(GotFocusEventArgs e) diff --git a/src/Perspex.Controls/TopLevel.cs b/src/Perspex.Controls/TopLevel.cs index e63e74ce20..b12dcbb33d 100644 --- a/src/Perspex.Controls/TopLevel.cs +++ b/src/Perspex.Controls/TopLevel.cs @@ -162,6 +162,10 @@ namespace Perspex.Controls styler?.ApplyStyles(this); this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => this.PlatformImpl.ClientSize = x); + this.GetObservable(TopLevel.PointerOverElementProperty) + .Select( + x => (x as InputElement)?.GetObservable(InputElement.CursorProperty) ?? Observable.Empty()) + .Switch().Subscribe(cursor => this.PlatformImpl.SetCursor(cursor?.PlatformCursor)); } /// diff --git a/src/Perspex.Input/Cursors.cs b/src/Perspex.Input/Cursors.cs new file mode 100644 index 0000000000..2262317aaa --- /dev/null +++ b/src/Perspex.Input/Cursors.cs @@ -0,0 +1,58 @@ +namespace Perspex.Input +{ + using Splat; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using Perspex.Platform; + + /* + ========================================================================================= + NOTE: Cursors are NOT disposable and are cached in platform implementation. + To support loading custom cursors some measures about that should be taken beforehand + ========================================================================================= + */ + + public enum StandardCursorType + { + Arrow, + Ibeam, + Wait, + Cross, + UpArrow, + SizeWestEast, + SizeNorthSouth, + SizeAll, + No, + Hand, + AppStarting, + Help + + // Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/ + // We might enable them later, preferably, by loading pixmax direclty from theme with fallback image + // SizeNorthWestSouthEast, + // SizeNorthEastSouthWest, + + } + + public class Cursor + { + public static Cursor Default = new Cursor(StandardCursorType.Arrow); + + internal Cursor(IPlatformHandle platformCursor) + { + this.PlatformCursor = platformCursor; + } + + public Cursor(StandardCursorType cursorType) + : this( + ((IStandardCursorFactory) Locator.Current.GetService(typeof(IStandardCursorFactory))).GetCursor( + cursorType)) + { + } + + public IPlatformHandle PlatformCursor { get; } + } +} diff --git a/src/Perspex.Input/IInputElement.cs b/src/Perspex.Input/IInputElement.cs index 3ce3c5d9d0..6126b1c973 100644 --- a/src/Perspex.Input/IInputElement.cs +++ b/src/Perspex.Input/IInputElement.cs @@ -4,6 +4,8 @@ // // ----------------------------------------------------------------------- +using System.Diagnostics.Contracts; + namespace Perspex.Input { using System; @@ -79,6 +81,11 @@ namespace Perspex.Input /// bool IsEnabled { get; } + /// + /// Gets or sets assicitated mouse cursor. + /// + Cursor Cursor { get; } + /// /// Gets a value indicating whether the control is effectively enabled for user interaction. /// diff --git a/src/Perspex.Input/InputElement.cs b/src/Perspex.Input/InputElement.cs index 154f71b32a..4aed80873c 100644 --- a/src/Perspex.Input/InputElement.cs +++ b/src/Perspex.Input/InputElement.cs @@ -35,6 +35,12 @@ namespace Perspex.Input public static readonly PerspexProperty IsEnabledCoreProperty = PerspexProperty.Register("IsEnabledCore", true); + /// + /// Gets or sets associated mouse cursor. + /// + public static readonly PerspexProperty CursorProperty = + PerspexProperty.Register("Cursor", null, true); + /// /// Defines the property. /// @@ -270,6 +276,15 @@ namespace Perspex.Input set { this.SetValue(IsEnabledProperty, value); } } + /// + /// Gets or sets associated mouse cursor. + /// + public Cursor Cursor + { + get { return this.GetValue(CursorProperty); } + set { this.SetValue(CursorProperty, value); } + } + /// /// Gets or sets a value indicating whether the control is focused. /// diff --git a/src/Perspex.Input/Perspex.Input.csproj b/src/Perspex.Input/Perspex.Input.csproj index 82aaf8b56a..fe66c25451 100644 --- a/src/Perspex.Input/Perspex.Input.csproj +++ b/src/Perspex.Input/Perspex.Input.csproj @@ -64,9 +64,11 @@ Properties\SharedAssemblyInfo.cs + + diff --git a/src/Perspex.Input/Platform/IStandardCursorFactory.cs b/src/Perspex.Input/Platform/IStandardCursorFactory.cs new file mode 100644 index 0000000000..7a18b60857 --- /dev/null +++ b/src/Perspex.Input/Platform/IStandardCursorFactory.cs @@ -0,0 +1,14 @@ +namespace Perspex.Platform +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using Perspex.Input; + + public interface IStandardCursorFactory + { + IPlatformHandle GetCursor(StandardCursorType cursorType); + } +} diff --git a/src/Windows/Perspex.Win32/CursorFactory.cs b/src/Windows/Perspex.Win32/CursorFactory.cs new file mode 100644 index 0000000000..5307fe135d --- /dev/null +++ b/src/Windows/Perspex.Win32/CursorFactory.cs @@ -0,0 +1,61 @@ +using Perspex.Win32.Interop; + +namespace Perspex.Win32 +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using Perspex.Input; + using Perspex.Platform; + + class CursorFactory : IStandardCursorFactory + { + public static CursorFactory Instance { get; } = new CursorFactory(); + + private CursorFactory() + { + } + + private static readonly Dictionary CursorTypeMapping = new Dictionary + + { + + { StandardCursorType.AppStarting, 32650 }, + { StandardCursorType.Arrow, 32512 }, + { StandardCursorType.Cross, 32515 }, + { StandardCursorType.Hand, 32649 }, + { StandardCursorType.Help, 32651 }, + { StandardCursorType.Ibeam, 32513 }, + { StandardCursorType.No, 32648 }, + { StandardCursorType.SizeAll, 32646 }, + + // { StandardCursorType.SizeNorthEastSouthWest, 32643 }, + { StandardCursorType.SizeNorthSouth, 32645 }, + + // { StandardCursorType.SizeNorthWestSouthEast, 32642 }, + { StandardCursorType.SizeWestEast, 32644 }, + { StandardCursorType.UpArrow, 32516 }, + { StandardCursorType.Wait, 32514 } + }; + + private static readonly Dictionary Cache = + new Dictionary(); + + public IPlatformHandle GetCursor(StandardCursorType cursorType) + { + IPlatformHandle rv; + if (!Cache.TryGetValue(cursorType, out rv)) + { + Cache[cursorType] = + rv = + new PlatformHandle( + UnmanagedMethods.LoadCursor(IntPtr.Zero, new IntPtr(CursorTypeMapping[cursorType])), + PlatformConstants.CursorHandleType); + } + + return rv; + } + } +} diff --git a/src/Windows/Perspex.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Perspex.Win32/Interop/UnmanagedMethods.cs index eaab2a9ed6..9f5c664008 100644 --- a/src/Windows/Perspex.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Perspex.Win32/Interop/UnmanagedMethods.cs @@ -572,7 +572,9 @@ namespace Perspex.Win32.Interop public static extern bool KillTimer(IntPtr hWnd, IntPtr uIDEvent); [DllImport("user32.dll")] - public static extern IntPtr LoadCursor(IntPtr hInstance, int lpCursorName); + public static extern IntPtr LoadCursor(IntPtr hInstance, IntPtr lpCursorName); + + [DllImport("user32.dll")] public static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg); @@ -627,6 +629,27 @@ namespace Perspex.Win32.Interop [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern bool SetWindowText(IntPtr hwnd, string lpString); + public enum ClassLongIndex : int + { + GCL_HCURSOR = -12, + GCL_HICON = -14 + } + + [DllImport("user32.dll", EntryPoint = "SetClassLongPtr")] + private static extern IntPtr SetClassLong64(IntPtr hWnd, ClassLongIndex nIndex, IntPtr dwNewLong); + + [DllImport("user32.dll", EntryPoint = "SetClassLong")] + private static extern IntPtr SetClassLong32(IntPtr hWnd, ClassLongIndex nIndex, IntPtr dwNewLong); + + public static IntPtr SetClassLong(IntPtr hWnd, ClassLongIndex nIndex, IntPtr dwNewLong) + { + if (IntPtr.Size == 4) + { + return SetClassLong32(hWnd, nIndex, dwNewLong); + } + + return SetClassLong64(hWnd, nIndex, dwNewLong); + } [DllImport("user32.dll", SetLastError = true)] public static extern bool OpenClipboard(IntPtr hWndOwner); diff --git a/src/Windows/Perspex.Win32/Perspex.Win32.csproj b/src/Windows/Perspex.Win32/Perspex.Win32.csproj index 3ce71ae46c..2c5f2ce9ef 100644 --- a/src/Windows/Perspex.Win32/Perspex.Win32.csproj +++ b/src/Windows/Perspex.Win32/Perspex.Win32.csproj @@ -67,10 +67,12 @@ + + diff --git a/src/Windows/Perspex.Win32/PlatformConstants.cs b/src/Windows/Perspex.Win32/PlatformConstants.cs new file mode 100644 index 0000000000..de9439624c --- /dev/null +++ b/src/Windows/Perspex.Win32/PlatformConstants.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Perspex.Win32 +{ + public static class PlatformConstants + { + public const string WindowHandleType = "HWND"; + public const string CursorHandleType = "HCURSOR"; + } +} diff --git a/src/Windows/Perspex.Win32/Win32Platform.cs b/src/Windows/Perspex.Win32/Win32Platform.cs index b753b3263d..f499b1cf8c 100644 --- a/src/Windows/Perspex.Win32/Win32Platform.cs +++ b/src/Windows/Perspex.Win32/Win32Platform.cs @@ -57,6 +57,7 @@ namespace Perspex.Win32 locator.Register(() => new ClipboardImpl(), typeof(IClipboard)); locator.Register(() => WindowsKeyboardDevice.Instance, typeof(IKeyboardDevice)); locator.Register(() => WindowsMouseDevice.Instance, typeof(IMouseDevice)); + locator.Register(() => CursorFactory.Instance, typeof(IStandardCursorFactory)); locator.Register(() => instance, typeof(IPlatformSettings)); locator.Register(() => instance, typeof(IPlatformThreadingInterface)); locator.RegisterConstant(new AssetLoader(), typeof(IAssetLoader)); diff --git a/src/Windows/Perspex.Win32/WindowImpl.cs b/src/Windows/Perspex.Win32/WindowImpl.cs index 89cdfc4356..bc6c00ce92 100644 --- a/src/Windows/Perspex.Win32/WindowImpl.cs +++ b/src/Windows/Perspex.Win32/WindowImpl.cs @@ -26,6 +26,9 @@ namespace Perspex.Win32 { private static List instances = new List(); + private static readonly IntPtr DefaultCursor = UnmanagedMethods.LoadCursor( + IntPtr.Zero, new IntPtr((int) UnmanagedMethods.Cursor.IDC_ARROW)); + private UnmanagedMethods.WndProc wndProcDelegate; private string className; @@ -185,6 +188,12 @@ namespace Perspex.Win32 }); } + public void SetCursor(IPlatformHandle cursor) + { + UnmanagedMethods.SetClassLong(this.hwnd, UnmanagedMethods.ClassLongIndex.GCL_HCURSOR, + cursor?.Handle ?? DefaultCursor); + } + protected virtual IntPtr CreateWindowOverride(ushort atom) { return UnmanagedMethods.CreateWindowEx( @@ -244,6 +253,7 @@ namespace Perspex.Win32 case UnmanagedMethods.WindowsMessage.WM_DESTROY: if (this.Closed != null) { + UnmanagedMethods.UnregisterClass(this.className, Marshal.GetHINSTANCE(this.GetType().Module)); this.Closed(); } @@ -384,7 +394,7 @@ namespace Perspex.Win32 style = 0, lpfnWndProc = this.wndProcDelegate, hInstance = Marshal.GetHINSTANCE(this.GetType().Module), - hCursor = UnmanagedMethods.LoadCursor(IntPtr.Zero, (int)UnmanagedMethods.Cursor.IDC_ARROW), + hCursor = DefaultCursor, hbrBackground = (IntPtr)5, lpszClassName = this.className, }; @@ -403,7 +413,7 @@ namespace Perspex.Win32 throw new Win32Exception(); } - this.Handle = new PlatformHandle(this.hwnd, "HWND"); + this.Handle = new PlatformHandle(this.hwnd, PlatformConstants.WindowHandleType); } private Point ScreenToClient(uint x, uint y)